From aea2e7e2fb243c57df805446474b845998a0ca2c Mon Sep 17 00:00:00 2001 From: Eser KUBALI Date: Wed, 22 Jul 2020 16:19:28 +0300 Subject: [PATCH] v0.1.3 Release Auto-generation capability was added for Petra based bots. Redundant petra based codes were removed. --- README.md | 4 +- install/arch.sh | 21 +- .../data.json => petra-Imperialist.data.json} | 0 .../data.json => petra-Patriot.data.json} | 0 .../data.json => petra-SingleBased.data.json} | 2 +- .../data.json => petra-Unitary.data.json} | 0 .../petraBased/petra-imperialist/_petrabot.js | 170 - .../petra-imperialist/attackPlan.js | 2176 ------------- .../petra-imperialist/headquarters.js | 2898 ---------------- .../petra-imperialist/queueManager.js | 542 --- .../petraBased/petra-patriot/attackManager.js | 805 ----- .../petraBased/petra-patriot/baseManager.js | 1111 ------- .../petraBased/petra-patriot/buildManager.js | 173 - .../petraBased/petra-patriot/chatHelper.js | 242 -- install/petraBased/petra-patriot/config.js | 257 -- .../petraBased/petra-patriot/defenseArmy.js | 651 ---- .../petra-patriot/defenseManager.js | 957 ------ .../petra-patriot/diplomacyManager.js | 559 ---- .../petraBased/petra-patriot/entityExtend.js | 441 --- .../petra-patriot/garrisonManager.js | 373 --- .../petraBased/petra-patriot/headquarters.js | 2900 ----------------- install/petraBased/petra-patriot/mapModule.js | 221 -- .../petraBased/petra-patriot/navalManager.js | 896 ----- install/petraBased/petra-patriot/queue.js | 167 - .../petraBased/petra-patriot/queueManager.js | 542 --- install/petraBased/petra-patriot/queueplan.js | 70 - .../petra-patriot/queueplanBuilding.js | 951 ------ .../petra-patriot/queueplanResearch.js | 113 - .../petra-patriot/queueplanTraining.js | 193 -- .../petra-patriot/researchManager.js | 244 -- .../petra-patriot/startingStrategy.js | 578 ---- .../petraBased/petra-patriot/tradeManager.js | 723 ---- .../petraBased/petra-patriot/transportPlan.js | 729 ----- .../petra-patriot/victoryManager.js | 748 ----- install/petraBased/petra-patriot/worker.js | 1114 ------- .../petraBased/petra-single-base/_petrabot.js | 172 - .../petra-single-base/attackManager.js | 805 ----- .../petra-single-base/attackPlan.js | 2171 ------------ .../petra-single-base/baseManager.js | 1111 ------- .../petra-single-base/buildManager.js | 173 - .../petra-single-base/chatHelper.js | 242 -- .../petraBased/petra-single-base/config.js | 257 -- .../petra-single-base/defenseArmy.js | 651 ---- .../petra-single-base/defenseManager.js | 957 ------ .../petra-single-base/diplomacyManager.js | 559 ---- .../petra-single-base/entityExtend.js | 441 --- .../petra-single-base/garrisonManager.js | 373 --- .../petraBased/petra-single-base/mapModule.js | 221 -- .../petra-single-base/navalManager.js | 896 ----- install/petraBased/petra-single-base/queue.js | 167 - .../petra-single-base/queueManager.js | 599 ---- .../petraBased/petra-single-base/queueplan.js | 70 - .../petra-single-base/queueplanBuilding.js | 951 ------ .../petra-single-base/queueplanResearch.js | 113 - .../petra-single-base/queueplanTraining.js | 193 -- .../petra-single-base/researchManager.js | 244 -- .../petra-single-base/startingStrategy.js | 578 ---- .../petra-single-base/tradeManager.js | 723 ---- .../petra-single-base/transportPlan.js | 729 ----- .../petra-single-base/victoryManager.js | 748 ----- .../petraBased/petra-single-base/worker.js | 1114 ------- install/petraBased/petra-unitary/_petrabot.js | 172 - .../petraBased/petra-unitary/attackManager.js | 805 ----- .../petraBased/petra-unitary/attackPlan.js | 2176 ------------- .../petraBased/petra-unitary/baseManager.js | 1111 ------- .../petraBased/petra-unitary/buildManager.js | 173 - .../petraBased/petra-unitary/chatHelper.js | 242 -- install/petraBased/petra-unitary/config.js | 257 -- .../petraBased/petra-unitary/defenseArmy.js | 651 ---- .../petra-unitary/defenseManager.js | 957 ------ .../petra-unitary/diplomacyManager.js | 559 ---- .../petraBased/petra-unitary/entityExtend.js | 441 --- .../petra-unitary/garrisonManager.js | 373 --- .../petraBased/petra-unitary/headquarters.js | 2900 ----------------- install/petraBased/petra-unitary/mapModule.js | 221 -- .../petraBased/petra-unitary/navalManager.js | 896 ----- install/petraBased/petra-unitary/queue.js | 167 - install/petraBased/petra-unitary/queueplan.js | 70 - .../petra-unitary/queueplanBuilding.js | 951 ------ .../petra-unitary/queueplanResearch.js | 113 - .../petra-unitary/queueplanTraining.js | 193 -- .../petra-unitary/researchManager.js | 244 -- .../petra-unitary/startingStrategy.js | 578 ---- .../petraBased/petra-unitary/tradeManager.js | 723 ---- .../petraBased/petra-unitary/transportPlan.js | 729 ----- .../petra-unitary/victoryManager.js | 748 ----- install/petraBased/petra-unitary/worker.js | 1114 ------- mod.json | 4 +- .../ai/petra-dev}/_petrabot.js | 8 + .../ai/petra-dev}/attackManager.js | 0 .../ai/petra-dev}/attackPlan.js | 14 +- .../ai/petra-dev}/baseManager.js | 0 .../ai/petra-dev}/buildManager.js | 0 .../ai/petra-dev}/chatHelper.js | 0 .../ai/petra-dev}/config.js | 0 simulation/ai/petra-dev/data.json | 7 + .../ai/petra-dev}/defenseArmy.js | 0 .../ai/petra-dev}/defenseManager.js | 0 .../ai/petra-dev}/diplomacyManager.js | 0 .../ai/petra-dev}/entityExtend.js | 0 .../ai/petra-dev}/garrisonManager.js | 0 .../ai/petra-dev}/headquarters.js | 10 + .../ai/petra-dev}/mapModule.js | 0 .../ai/petra-dev}/navalManager.js | 0 .../ai/petra-dev}/queue.js | 0 .../ai/petra-dev}/queueManager.js | 19 +- .../ai/petra-dev}/queueplan.js | 0 .../ai/petra-dev}/queueplanBuilding.js | 0 .../ai/petra-dev}/queueplanResearch.js | 0 .../ai/petra-dev}/queueplanTraining.js | 0 .../ai/petra-dev}/researchManager.js | 0 .../ai/petra-dev}/startingStrategy.js | 0 .../ai/petra-dev}/tradeManager.js | 0 .../ai/petra-dev}/transportPlan.js | 0 .../ai/petra-dev}/victoryManager.js | 0 .../ai/petra-dev}/worker.js | 0 116 files changed, 78 insertions(+), 54377 deletions(-) rename install/{petraBased/petra-imperialist/data.json => petra-Imperialist.data.json} (100%) rename install/{petraBased/petra-patriot/data.json => petra-Patriot.data.json} (100%) rename install/{petraBased/petra-single-base/data.json => petra-SingleBased.data.json} (93%) rename install/{petraBased/petra-unitary/data.json => petra-Unitary.data.json} (100%) delete mode 100644 install/petraBased/petra-imperialist/_petrabot.js delete mode 100644 install/petraBased/petra-imperialist/attackPlan.js delete mode 100644 install/petraBased/petra-imperialist/headquarters.js delete mode 100644 install/petraBased/petra-imperialist/queueManager.js delete mode 100644 install/petraBased/petra-patriot/attackManager.js delete mode 100644 install/petraBased/petra-patriot/baseManager.js delete mode 100644 install/petraBased/petra-patriot/buildManager.js delete mode 100644 install/petraBased/petra-patriot/chatHelper.js delete mode 100644 install/petraBased/petra-patriot/config.js delete mode 100644 install/petraBased/petra-patriot/defenseArmy.js delete mode 100644 install/petraBased/petra-patriot/defenseManager.js delete mode 100644 install/petraBased/petra-patriot/diplomacyManager.js delete mode 100644 install/petraBased/petra-patriot/entityExtend.js delete mode 100644 install/petraBased/petra-patriot/garrisonManager.js delete mode 100644 install/petraBased/petra-patriot/headquarters.js delete mode 100644 install/petraBased/petra-patriot/mapModule.js delete mode 100644 install/petraBased/petra-patriot/navalManager.js delete mode 100644 install/petraBased/petra-patriot/queue.js delete mode 100644 install/petraBased/petra-patriot/queueManager.js delete mode 100644 install/petraBased/petra-patriot/queueplan.js delete mode 100644 install/petraBased/petra-patriot/queueplanBuilding.js delete mode 100644 install/petraBased/petra-patriot/queueplanResearch.js delete mode 100644 install/petraBased/petra-patriot/queueplanTraining.js delete mode 100644 install/petraBased/petra-patriot/researchManager.js delete mode 100644 install/petraBased/petra-patriot/startingStrategy.js delete mode 100644 install/petraBased/petra-patriot/tradeManager.js delete mode 100644 install/petraBased/petra-patriot/transportPlan.js delete mode 100644 install/petraBased/petra-patriot/victoryManager.js delete mode 100644 install/petraBased/petra-patriot/worker.js delete mode 100644 install/petraBased/petra-single-base/_petrabot.js delete mode 100644 install/petraBased/petra-single-base/attackManager.js delete mode 100644 install/petraBased/petra-single-base/attackPlan.js delete mode 100644 install/petraBased/petra-single-base/baseManager.js delete mode 100644 install/petraBased/petra-single-base/buildManager.js delete mode 100644 install/petraBased/petra-single-base/chatHelper.js delete mode 100644 install/petraBased/petra-single-base/config.js delete mode 100644 install/petraBased/petra-single-base/defenseArmy.js delete mode 100644 install/petraBased/petra-single-base/defenseManager.js delete mode 100644 install/petraBased/petra-single-base/diplomacyManager.js delete mode 100644 install/petraBased/petra-single-base/entityExtend.js delete mode 100644 install/petraBased/petra-single-base/garrisonManager.js delete mode 100644 install/petraBased/petra-single-base/mapModule.js delete mode 100644 install/petraBased/petra-single-base/navalManager.js delete mode 100644 install/petraBased/petra-single-base/queue.js delete mode 100644 install/petraBased/petra-single-base/queueManager.js delete mode 100644 install/petraBased/petra-single-base/queueplan.js delete mode 100644 install/petraBased/petra-single-base/queueplanBuilding.js delete mode 100644 install/petraBased/petra-single-base/queueplanResearch.js delete mode 100644 install/petraBased/petra-single-base/queueplanTraining.js delete mode 100644 install/petraBased/petra-single-base/researchManager.js delete mode 100644 install/petraBased/petra-single-base/startingStrategy.js delete mode 100644 install/petraBased/petra-single-base/tradeManager.js delete mode 100644 install/petraBased/petra-single-base/transportPlan.js delete mode 100644 install/petraBased/petra-single-base/victoryManager.js delete mode 100644 install/petraBased/petra-single-base/worker.js delete mode 100644 install/petraBased/petra-unitary/_petrabot.js delete mode 100644 install/petraBased/petra-unitary/attackManager.js delete mode 100644 install/petraBased/petra-unitary/attackPlan.js delete mode 100644 install/petraBased/petra-unitary/baseManager.js delete mode 100644 install/petraBased/petra-unitary/buildManager.js delete mode 100644 install/petraBased/petra-unitary/chatHelper.js delete mode 100644 install/petraBased/petra-unitary/config.js delete mode 100644 install/petraBased/petra-unitary/defenseArmy.js delete mode 100644 install/petraBased/petra-unitary/defenseManager.js delete mode 100644 install/petraBased/petra-unitary/diplomacyManager.js delete mode 100644 install/petraBased/petra-unitary/entityExtend.js delete mode 100644 install/petraBased/petra-unitary/garrisonManager.js delete mode 100644 install/petraBased/petra-unitary/headquarters.js delete mode 100644 install/petraBased/petra-unitary/mapModule.js delete mode 100644 install/petraBased/petra-unitary/navalManager.js delete mode 100644 install/petraBased/petra-unitary/queue.js delete mode 100644 install/petraBased/petra-unitary/queueplan.js delete mode 100644 install/petraBased/petra-unitary/queueplanBuilding.js delete mode 100644 install/petraBased/petra-unitary/queueplanResearch.js delete mode 100644 install/petraBased/petra-unitary/queueplanTraining.js delete mode 100644 install/petraBased/petra-unitary/researchManager.js delete mode 100644 install/petraBased/petra-unitary/startingStrategy.js delete mode 100644 install/petraBased/petra-unitary/tradeManager.js delete mode 100644 install/petraBased/petra-unitary/transportPlan.js delete mode 100644 install/petraBased/petra-unitary/victoryManager.js delete mode 100644 install/petraBased/petra-unitary/worker.js rename {install/petraBased/petra-patriot => simulation/ai/petra-dev}/_petrabot.js (97%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/attackManager.js (100%) rename {install/petraBased/petra-patriot => simulation/ai/petra-dev}/attackPlan.js (99%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/baseManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/buildManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/chatHelper.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/config.js (100%) create mode 100644 simulation/ai/petra-dev/data.json rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/defenseArmy.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/defenseManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/diplomacyManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/entityExtend.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/garrisonManager.js (100%) rename {install/petraBased/petra-single-base => simulation/ai/petra-dev}/headquarters.js (99%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/mapModule.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/navalManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/queue.js (100%) rename {install/petraBased/petra-unitary => simulation/ai/petra-dev}/queueManager.js (95%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/queueplan.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/queueplanBuilding.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/queueplanResearch.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/queueplanTraining.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/researchManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/startingStrategy.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/tradeManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/transportPlan.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/victoryManager.js (100%) rename {install/petraBased/petra-imperialist => simulation/ai/petra-dev}/worker.js (100%) diff --git a/README.md b/README.md index 3ee05be..7f9159d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Arch based AI bots use Arch AI architecture. Petra based AI bots use the original Petra AI architecture. - Imperialist - Patriot -- Single Base +- Single Based - Unitary ## Arch AI Architecture @@ -77,7 +77,7 @@ Arch AI changes its personality during the game according to the population from ### For Users You can download the release versions from release directory. Extract the release version and copy the ArchAIPack directory to the mods directory ( ../0ad/data/mods/ ). ### For Developers -Arch install script generates 9 Arch based AI bots (Admiral, Capitalist, Communist, Imperialist, Mason, Mercantilist, Patriot, Theocrat and Unitary) and copies preconfigured 4 Petra based AI bots (Imperialist, Patriot, Single Base and Unitary). +Arch install script generates 9 Arch based AI bots (Admiral, Capitalist, Communist, Imperialist, Mason, Mercantilist, Patriot, Theocrat and Unitary) and 4 Petra based AI bots (Imperialist, Patriot, Single Based and Unitary). ~~~~ cd install sh arch.sh diff --git a/install/arch.sh b/install/arch.sh index 1636f97..dfaf2d0 100644 --- a/install/arch.sh +++ b/install/arch.sh @@ -5,10 +5,12 @@ mkdir -p ../release cp ../mod.json ArchAIPack archDevDirectory="../simulation/ai/arch-dev" +petraDevDirectory="../simulation/ai/petra-dev" -declare -a bots=("Admiral" "Capitalist" "Communist" "Imperialist" "Mercantilist" "Patriot" "Mason" "Unitary" "Theocrat") +declare -a archBots=("Admiral" "Capitalist" "Communist" "Imperialist" "Mercantilist" "Patriot" "Mason" "Unitary" "Theocrat") +declare -a petraBots=("Imperialist" "Patriot" "SingleBased" "Unitary") -for ai in ${bots[@]} +for ai in ${archBots[@]} do targetDirectory="ArchAIPack/simulation/ai/arch-"$(echo ${ai}| tr '[:upper:][I]' '[:lower:][i]') mkdir -p ${targetDirectory} @@ -27,7 +29,20 @@ do cp -f arch-${ai}.data.json ${targetDirectory}/data.json done -cp -fR petraBased/* ArchAIPack/simulation/ai/ +for ai in ${petraBots[@]} +do + targetDirectory="ArchAIPack/simulation/ai/petra-"$(echo ${ai}| tr '[:upper:][I]' '[:lower:][i]') + mkdir -p ${targetDirectory} + + petraSourceFiles=$(ls -1 ${petraDevDirectory}); + + for file in ${petraSourceFiles} + do + cat ${petraDevDirectory}/${file}|awk -v target=${ai} 'BEGIN {c=1;}{if($2!=target){if($1=="///")c*=-1;else if(c==1)print $0;}}' > ${targetDirectory}/${file} + done + + cp -f petra-${ai}.data.json ${targetDirectory}/data.json +done version=$(cat ../mod.json|grep "\"version\":"|awk {'print $2'}|tr -d "\",") diff --git a/install/petraBased/petra-imperialist/data.json b/install/petra-Imperialist.data.json similarity index 100% rename from install/petraBased/petra-imperialist/data.json rename to install/petra-Imperialist.data.json diff --git a/install/petraBased/petra-patriot/data.json b/install/petra-Patriot.data.json similarity index 100% rename from install/petraBased/petra-patriot/data.json rename to install/petra-Patriot.data.json diff --git a/install/petraBased/petra-single-base/data.json b/install/petra-SingleBased.data.json similarity index 93% rename from install/petraBased/petra-single-base/data.json rename to install/petra-SingleBased.data.json index d8850e8..e67cd2e 100644 --- a/install/petraBased/petra-single-base/data.json +++ b/install/petra-SingleBased.data.json @@ -1,5 +1,5 @@ { - "name": "Petra ( Single Base )", + "name": "Petra ( Single Based )", "description": "Petra is the default 0 A.D. AI bot. This type of Petra Bot does not expand by building multiple bases. Please report issues to Wildfire Games (see the link in the main menu).\n\nThe AI has a bonus/penalty on resource stockpiling (either gathering rate or trade gain) varying from -60% for Sandbox to +60% for Very Hard (Medium 0%) and the easiest levels have a slower production and building rate. In addition, the Sandbox level does not expand nor attack.", "moduleName": "PETRA", "constructor": "PetraBot", diff --git a/install/petraBased/petra-unitary/data.json b/install/petra-Unitary.data.json similarity index 100% rename from install/petraBased/petra-unitary/data.json rename to install/petra-Unitary.data.json diff --git a/install/petraBased/petra-imperialist/_petrabot.js b/install/petraBased/petra-imperialist/_petrabot.js deleted file mode 100644 index 9cdc40b..0000000 --- a/install/petraBased/petra-imperialist/_petrabot.js +++ /dev/null @@ -1,170 +0,0 @@ -Engine.IncludeModule("common-api"); - -var PETRA = (function() { -var m = {}; - -m.PetraBot = function PetraBot(settings) -{ - API3.BaseAI.call(this, settings); - - 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 m.Config(settings.difficulty, settings.behavior); - - this.savedEvents = {}; -}; - -m.PetraBot.prototype = new API3.BaseAI(); - -m.PetraBot.prototype.CustomInit = function(gameState) -{ - if (this.isDeserialized) - { - // WARNING: the deserializations should not modify the metadatas infos inside their init functions - this.turn = this.data.turn; - this.playedTurn = this.data.playedTurn; - this.elapsedTime = this.data.elapsedTime; - this.savedEvents = this.data.savedEvents; - for (let key in this.savedEvents) - { - for (let i in this.savedEvents[key]) - { - if (!this.savedEvents[key][i].entityObj) - continue; - let evt = this.savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - { - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = new API3.Entity(gameState.sharedScript, evt.entityObj); - this.savedEvents[key][i] = evtmod; - } - } - } - - this.Config.Deserialize(this.data.config); - - this.queueManager = new m.QueueManager(this.Config, {}); - this.queueManager.Deserialize(gameState, this.data.queueManager); - this.queues = this.queueManager.queues; - - this.HQ = new m.HQ(this.Config); - 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 (let i in this.Config.priorities) - this.queues[i] = new m.Queue(); - - this.queueManager = new m.QueueManager(this.Config, this.queues); - - this.HQ = new m.HQ(this.Config); - - this.HQ.init(gameState, this.queues); - - // Analyze our starting position and set a strategy - this.HQ.gameAnalysis(gameState); - } -}; - -m.PetraBot.prototype.OnUpdate = function(sharedScript) -{ - if (this.gameFinished) - return; - - for (let i in this.events) - { - if (i == "AIMetadata") // not used inside petra - continue; - if(this.savedEvents[i] !== undefined) - this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]); - else - this.savedEvents[i] = this.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.gameState.getOwnEntities().length === 0) - { - Engine.ProfileStop(); - return; // With no entities to control the AI cannot do anything - } - - this.HQ.update(this.gameState, this.queues, this.savedEvents); - - this.queueManager.update(this.gameState); - - for (let i in this.savedEvents) - this.savedEvents[i] = []; - - Engine.ProfileStop(); - } - - this.turn++; -}; - -m.PetraBot.prototype.Serialize = function() -{ - let savedEvents = {}; - for (let key in this.savedEvents) - { - savedEvents[key] = this.savedEvents[key].slice(); - for (let i in savedEvents[key]) - { - if (!savedEvents[key][i].entityObj) - continue; - let evt = savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = evt.entityObj._entity; - savedEvents[key][i] = evtmod; - } - } - - return { - "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() - }; -}; - -m.PetraBot.prototype.Deserialize = function(data, sharedScript) -{ - this.isDeserialized = true; - this.data = data; -}; - -return m; -}()); diff --git a/install/petraBased/petra-imperialist/attackPlan.js b/install/petraBased/petra-imperialist/attackPlan.js deleted file mode 100644 index 677fd55..0000000 --- a/install/petraBased/petra-imperialist/attackPlan.js +++ /dev/null @@ -1,2176 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This is an attack plan: - * It deals with everything in an attack, from picking a target to picking a path to it - * To making sure units are built, and pushing elements to the queue manager otherwise - * It also handles the actual attack, though much work is needed on that. - */ - -m.AttackPlan = function(gameState, Config, uniqueID, type, data) -{ - this.Config = Config; - this.name = uniqueID; - this.type = type || "Attack"; - this.state = "unexecuted"; - this.forced = false; // true when this attacked has been forced to help an ally - - if (data && data.target) - { - 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.uniqueTargetId = data && data.uniqueTargetId || undefined; - - // get a starting rallyPoint ... will be improved later - let rallyPoint; - let rallyAccess; - let allAccesses = {}; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let access = m.getLandAccess(gameState, base.anchor); - if (!rallyPoint) - { - rallyPoint = base.anchor.position(); - rallyAccess = access; - } - if (!allAccesses[access]) - allAccesses[access] = base.anchor.position(); - } - if (!rallyPoint) // no base ? take the position of any of our entities - { - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position()) - continue; - let access = m.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 (let structure of gameState.getEnemyStructures().values()) - { - if (this.target && structure.id() != this.target.id()) - continue; - if (!structure.position()) - continue; - let access = m.getLandAccess(gameState, structure); - if (access in allAccesses) - { - this.overseas = 0; - this.rallyPoint = allAccesses[access]; - break; - } - else if (!this.overseas) - { - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); - if (!sea) - { - if (this.target) - { - API3.warn("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 = 70; - - // 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 == "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - if (data && data.targetSize) - this.unitStat.Infantry.targetSize = data.targetSize; - this.neededShips = 1; - } - else if (type == "Raid") - { - priority = 150; - this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [ ["strength", 1] ] }; - this.neededShips = 1; - } - else if (type == "HugeAttack") - { - 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.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], - "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - this.neededShips = 3; - } - - // Put some randomness on the attack size - let variation = randFloat(0.8, 1.2); - // and lower priority and smaller sizes for easier difficulty levels - if (this.Config.difficulty < 2) - { - priority *= 0.6; - variation *= 0.5; - } - else if (this.Config.difficulty < 3) - { - priority *= 0.8; - variation *= 0.8; - } - for (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(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 (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); - } - - // 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 = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders - - // 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; -}; - -m.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(API3.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 (let cat in this.unitStat) - { - let Unit = this.unitStat[cat]; - this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[cat].registerUpdates(); - if (this.canBuildUnits) - this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); - } -}; - -m.AttackPlan.prototype.getName = function() -{ - return this.name; -}; - -m.AttackPlan.prototype.getType = function() -{ - return this.type; -}; - -m.AttackPlan.prototype.isStarted = function() -{ - return this.state !== "unexecuted" && this.state !== "completing"; -}; - -m.AttackPlan.prototype.isPaused = function() -{ - return this.paused; -}; - -m.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. - */ -m.AttackPlan.prototype.canStart = function() -{ - if (!this.canBuildUnits) - return true; - - for (let unitCat in this.unitStat) - if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) - return false; - - return true; -}; - -m.AttackPlan.prototype.mustStart = function() -{ - if (this.isPaused()) - return false; - - if (!this.canBuildUnits) - return this.unitCollection.hasEntities(); - - let MaxReachedEverywhere = true; - let MinReachedEverywhere = true; - for (let unitCat in this.unitStat) - { - let 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 == "Raid" && this.target && this.target.foundationProgress() && - this.target.foundationProgress() > 50; - return false; -}; - -m.AttackPlan.prototype.forceStart = function() -{ - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - Unit.targetSize = 0; - Unit.minSize = 0; - } - this.forced = true; -}; - -/** Adds a build order. If resetQueue is true, this will reset the queue. */ -m.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; - let Unit = this.unitStat[name]; - this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[name].registerUpdates(); - this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); - if (resetQueue) - { - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - } -}; - -m.AttackPlan.prototype.addSiegeUnits = function(gameState) -{ - if (this.siegeState == 2 || this.state !== "unexecuted") - return false; - - let civ = gameState.getPlayerCiv(); - let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; - let hasTrainer = [false, false, false]; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - let trainables = ent.trainableEntities(civ); - if (!trainables) - continue; - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.available(gameState)) - continue; - for (let i = 0; i < classes.length; ++i) - if (classes[i].every(c => template.hasClass(c))) - hasTrainer[i] = true; - } - } - if (hasTrainer.every(e => !e)) - return false; - let i = this.name % classes.length; - for (let k = 0; k < classes.length; ++k) - { - if (hasTrainer[i]) - break; - i = ++i % classes.length; - } - - this.siegeState = 2; - let targetSize; - if (this.Config.difficulty < 3) - targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); - else - targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; - targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); - if (!targetSize) - return true; - // no minsize as we don't want the plan to fail at the last minute though. - let 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". */ -m.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 == "completing") - { - // if our target was destroyed, go back to "unexecuted" state - if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) - { - this.state = "unexecuted"; - this.target = undefined; - } - else - { - // check that all units have finished with their transport if needed - if (this.waitingForTransport()) - return 1; - // bloqued units which cannot finish their order should not stop the attack - if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) - return 1; - return 2; - } - } - - 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 1; - - if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) - this.assignUnits(gameState); - if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) - this.reassignCavUnit(gameState); // reassign some cav (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.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - else // Abort the plan so that its units will be reassigned to other plans. - { - if (this.Config.debug > 1) - { - let am = gameState.ai.HQ.attackManager; - API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + - " rush " + am.upcomingAttacks.Rush.length + - " attack " + am.upcomingAttacks.Attack.length + - " huge " + am.upcomingAttacks.HugeAttack.length); - API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + - " rush " + am.startedAttacks.Rush.length + - " attack " + am.startedAttacks.Attack.length + - " huge " + am.startedAttacks.HugeAttack.length); - } - return 0; - } - } - else if (this.mustStart()) - { - if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) - { - // keep on while the units finish being trained, then we'll start - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - return 1; - } - } - else - { - if (this.canBuildUnits) - { - // We still have time left to recruit units and do stuffs. - if (this.siegeState == 0 || this.siegeState == 1 && 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 0; // will abort the plan - } - return 1; - } - - // if we're here, it means we must start - this.state = "completing"; - - // Raids have their predefined target - if (!this.target && !this.chooseTarget(gameState)) - return 0; - if (!this.overseas) - this.getPathToTarget(gameState); - - if (this.type == "Raid") - this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; - else - { - if (this.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 (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) - continue; - unitIds.delete(entId); - ent.stopMoving(); - } - } - - let rallyPoint = this.rallyPoint; - let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); - for (let 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", "attack"); - ent.setMetadata(PlayerID, "subrole", "completing"); - let queued = false; - if (ent.resourceCarrying() && ent.resourceCarrying().length) - queued = m.returnResources(gameState, ent); - let index = m.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 - let plan = this.name; - gameState.ai.queueManager.removeQueue("plan_" + plan); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); - return 1; -}; - - -m.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 (let order of this.buildOrders) - { - let 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; - return va - vb; - }); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - { - API3.warn("===================================="); - API3.warn("======== build order for plan " + this.name); - for (let order of this.buildOrders) - { - let specialData = "Plan_"+this.name+"_"+order[4]; - let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); - let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); - let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); - let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); - API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + - " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); - } - API3.warn("===================================="); - } - - let firstOrder = this.buildOrders[0]; - if (firstOrder[0] < firstOrder[3].targetSize) - { - // find the actual queue we want - let queue = this.queue; - if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && - firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) - 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) - { - let 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) - API3.warn("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) - API3.warn("attack template " + template + " added for plan " + this.name); - let max = firstOrder[3].batchSize; - let specialData = "Plan_" + this.name + "_" + firstOrder[4]; - let data = { "plan": this.name, "special": specialData, "base": 0 }; - data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; - let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); - if (trainingPlan.template) - queue.addPlan(trainingPlan); - else if (this.Config.debug > 1) - API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + - " build3 " + uneval(firstOrder[3].interests)); - } - } - } -}; - -m.AttackPlan.prototype.assignUnits = function(gameState) -{ - let 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 (let 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 == "Raid") - { - // Raid are fast cavalry attack: assign all cav except some for hunting - let num = 0; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Cavalry") || !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 (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) - { - if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) - continue; - if (ent.hasClass("Ship") || ent.hasClass("Support") || 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 (let 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, - // 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 - let num = 0; - let numbase = {}; - let keep = this.type != "Rush" ? - 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; - keep = Math.round(this.Config.popScaling * keep); - for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) - { - if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) - continue; - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID) - numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; - else - { - API3.warn("Petra problem ent without base "); - m.dumpEntity(ent); - continue; - } - if (num++ < keep || numbase[baseID] < 5) - continue; - if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; -}; - -m.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) Cav unit to fasten raid preparation. */ -m.AttackPlan.prototype.reassignCavUnit = function(gameState) -{ - for (let ent of this.unitCollection.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) - continue; - let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); - ent.setMetadata(PlayerID, "plan", raid.name); - this.unitCollection.updateEnt(ent); - raid.unitCollection.updateEnt(ent); - return; - } -}; - -m.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) - let targetIndex = m.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 (let base of gameState.ai.HQ.baseManagers) - { - let anchor = base.anchor; - if (!anchor || !anchor.position()) - continue; - let dist = API3.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; - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); - if (sea) - { - this.overseas = sea; - gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); - } - else - { - API3.warn("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 - */ -m.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 API3.EntityCollection(gameState.sharedScript); - let ent = gameState.getEntityById(this.uniqueTargetId); - if (ent) - targets.addEnt(ent); - } - else - { - if (this.type == "Raid") - targets = this.raidTargetFinder(gameState); - else if (this.type == "Rush" || this.type == "Attack") - { - 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 (let 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 = API3.SquareVectorDistance(ent.position(), position); - // In normal attacks, disfavor fields - if (this.type != "Rush" && this.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 - */ -m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - if (gameState.getVictoryConditions().has("wonder")) - for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("regicide")) - for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("capture_the_relic")) - for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) - targets.addEnt(ent); - targets = targets.filter(this.isValidTarget, this); - if (targets.hasEntities()) - return targets; - - let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); - targets = validTargets.filter(API3.Filters.byClass("CivCentre")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); - // If there's nothing, attack anything else that's less critical - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("Town")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.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(API3.Filters.byClass("ConquestCritical")). - filter(API3.Filters.not(API3.Filters.byClass("Ship"))); - return targets; -}; - -m.AttackPlan.prototype.isValidTarget = function(ent) -{ - if (!ent.position()) - return false; - if (this.sameLand && m.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 */ -m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.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 (let building of buildings) - { - if (building.owner() == 0) - continue; - if (building.hasDefensiveFire()) - continue; - if (!this.isValidTarget(building)) - continue; - let pos = building.position(); - let defended = false; - for (let defense of buildings) - { - if (!defense.hasDefensiveFire()) - continue; - let dist = API3.SquareVectorDistance(pos, defense.position()); - if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 - { - defended = true; - break; - } - } - if (defended) - continue; - let dist = API3.SquareVectorDistance(pos, this.position); - if (dist > minDist) - continue; - minDist = dist; - target = building; - } - if (target) - targets.addEnt(target); - - if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) - targets = this.rushTargetFinder(gameState); - - return targets; -}; - -/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ -m.AttackPlan.prototype.raidTargetFinder = function(gameState) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - let 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 - */ -m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) -{ - if (m.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) - return target; - - let targetPos = target.position(); - let startPos = { "x": position[0], "y": position[1] }; - let endPos = { "x": targetPos[0], "y": targetPos[1] }; - let blocker; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); - if (!path.length) - return undefined; - - let pathPos = [path[0].x, path[0].y]; - let dist = API3.VectorDistance(pathPos, targetPos); - let radius = target.obstructionRadius().max; - for (let 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("Gates")) - 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. - let structPos = struct.position(); - let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; - let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; - - if (struct.get("Obstruction/Static")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Static/@width"); - let depth = +struct.get("Obstruction/Static/@depth"); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - } - else if (struct.get("Obstruction/Obstructions")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Obstructions/Door/@width"); - let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); - let 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")); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - // check that the path does not cross this gate (could happen if not locked) - for (let i = 1; i < path.length; ++i) - { - let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; - let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; - let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; - let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; - if (v1 * v2 < 0) - { - let 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")) - { - let r = +this.get("Obstruction/Unit/@radius"); - if (x*x + y*y < r*r) - { - blocker = struct; - break; - } - } - } - - if (blocker && blocker.hasClass("StoneWall")) - { -/* if (this.hasSiegeUnits()) - { */ - this.isBlocked = true; - return blocker; -/* } - return undefined; */ - } - else if (blocker) - { - this.isBlocked = true; - return blocker; - } - - return target; -}; - -m.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) -{ - let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let endAccess = m.getLandAccess(gameState, this.target); - if (startAccess != endAccess) - return false; - - Engine.ProfileStart("AI Compute path"); - let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; - let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); - this.path = []; - this.path.push(this.targetPos); - for (let p in path) - this.path.push([path[p].x, path[p].y]); - this.path.push(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 */ -m.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. - */ -m.AttackPlan.prototype.StartAttack = function(gameState) -{ - - if (this.Config.debug > 1) - API3.warn("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. - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); - - // Wait to attack! - if( gameState.getPopulation() < 0.85*gameState.getPopulationMax() ) - return false; - - for (let ent of this.unitCollection.values()) - { - ent.setMetadata(PlayerID, "subrole", "walking"); - let stance = ent.isPackable() ? "standground" : "aggressive"; - if (ent.getStance() != stance) - ent.setStance(stance); - } - - let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let targetAccess = m.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 (let 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 */ -m.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.state == "walking" && !this.UpdateWalking(gameState, events)) - { - Engine.ProfileStop(); - return 0; - } - - if (this.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", "attacking"); - }); - if (this.type == "Rush") // try to find a better target for rush - { - let 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; - } - - let time = gameState.ai.elapsedTime; - let attackedByStructure = {}; - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - let ourUnit = gameState.getEntityById(evt.target); - if (!ourUnit || !attacker || !attacker.position()) - continue; - if (!attacker.hasClass("Unit")) - { - attackedByStructure[evt.target] = true; - continue; - } - if (m.isSiegeUnit(ourUnit)) - { // if our siege units are attacked, we'll send some units to deal with enemies. - let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - // And if this attacker is a non-ranged siege unit and our unit also, attack it - if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) - { - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) - { - // 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 (m.isSiegeUnit(attacker)) - { // if our unit is attacked by a siege unit, we'll send some melee units to help it. - let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - // Look first for nearby units to help us if possible - let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) - continue; - let orderData = ent.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - continue; - } - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - 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 - let orderData = ourUnit.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - { - if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) - continue; - } - } - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - } - - let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); - let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); - - // Count the number of times an enemy is targeted, to prevent all units to follow the same target - let unitTargets = {}; - for (let ent of this.unitCollection.values()) - { - if (ent.hasClass("Ship")) // TODO What to do with ships - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || target.hasClass("Structure")) - continue; - if (!(targetId in unitTargets)) - { - if (m.isSiegeUnit(target) || target.hasClass("Hero")) - unitTargets[targetId] = -8; - else if (target.hasClass("Champion") || target.hasClass("Ship")) - unitTargets[targetId] = -5; - else - unitTargets[targetId] = -3; - } - ++unitTargets[targetId]; - } - let veto = {}; - for (let target in unitTargets) - if (unitTargets[target] > 0) - veto[target] = true; - - let targetClassesUnit; - let targetClassesSiege; - if (this.type == "Rush") - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto }; - else - { - if (this.target.hasClass("Fortress")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto }; - else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; - else - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "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", "Blacksmith"); - targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); - } - - 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 - let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; - for (let check = 0; check < lgth; check++) - { - let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); - if (!ent || !ent.position()) - continue; - // Do not reaffect units which have reacted to an attack in that same turn - if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) - continue; - - let targetId; - let 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; - let siegeUnit = m.isSiegeUnit(ent); - if (ent.isIdle()) - needsUpdate = true; - else if (siegeUnit && targetId) - { - let 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) - { - let 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("Cavalry") && !ent.hasClass("Ranged") && - target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") - maybeUpdate = true; - } - - // don't update too soon if not necessary - if (!needsUpdate) - { - if (!maybeUpdate) - continue; - let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; - let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); - if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) - continue; - } - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - let range = 60; - let 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("Cavalry")) - range += 30; - range = range * range; - let entAccess = m.getLandAccess(gameState, ent); - // Checking for gates if we're a siege unit. - if (siegeUnit) - { - let mStruct = enemyStructures.filter(enemy => { - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (enemy.foundationProgress() == 0) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasDefensiveFire()) - vala += 1000; - else if (structa.hasClass("ConquestCritical")) - vala += 200; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasDefensiveFire()) - valb += 1000; - else if (structb.hasClass("ConquestCritical")) - valb += 200; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); - } - } - else - { - if (!ent.hasClass("Ranged")) - { - let 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 - { - let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); - let mUnit = enemyUnits.filter(enemy => { - if (!enemy.position()) - return false; - if (enemy.hasClass("Animal")) - return false; - if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") - return false; - let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); - if (dist > range) - return false; - if (m.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()] && API3.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.countersClasses(unitA.classes())) - vala += 100; - let valb = unitB.hasClass("Support") ? 50 : 0; - if (ent.countersClasses(unitB.classes())) - valb += 100; - let distA = unitA.getMetadata(PlayerID, "distance"); - let 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; - }); - let rand = randIntExclusive(0, mUnit.length * 0.1); - ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); - } - else if (this.isBlocked) - ent.attack(this.target.id(), false); - else if (API3.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.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; - else - targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; - } - else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); - } - else - { - let mStruct = enemyStructures.filter(enemy => { - if (this.isBlocked && enemy.id() != this.target.id()) - return false; - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }, this).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasClass("ConquestCritical")) - vala += 100; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasClass("ConquestCritical")) - valb += 100; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), false); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.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; - if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) - return; - let dist = API3.SquareVectorDistance(unit.position(), ent.position()); - if (dist > distmin) - return; - distmin = dist; - attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); - }); - if (attacker) - ent.attack(attacker.id(), m.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.lastPosition = this.position; - Engine.ProfileStop(); - - return this.unitCollection.length; -}; - -m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) -{ - let done = true; - for (let 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; - } - - if (done) - { - this.state = "arrived"; - return; - } - - // if we are attacked while waiting the rest of the army, retaliate - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - if (!attacker || !gameState.getEntityById(evt.target)) - continue; - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.isIdle()) - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - } - break; - } -}; - -m.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 (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let 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 = "arrived"; - return true; - } - } - - // basically haven't moved an inch: very likely stuck) - if (API3.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 (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) - { - let dist = API3.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 = this.position; - - if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) - { - if (!this.path[0][0] || !this.path[0][1]) - API3.warn("Start: Problem with path " + uneval(this.path)); - // We're stuck, presumably. Check if there are no walls just close to us. - for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) - { - if (API3.SquareVectorDistance(this.position, ent.position()) > 800) - continue; - let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; - // there are walls, so check if we can attack - if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); - this.state = "arrived"; - return true; - } - // abort plan - if (this.Config.debug > 1) - API3.warn("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 (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) - { - 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) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - } - - return true; -}; - -m.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) - { - let holder = m.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) - API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); - let accessIndex = this.getAttackAccess(gameState); - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.uniqueTargetId) - return false; - - // Check if we could help any current attack - let attackManager = gameState.ai.HQ.attackManager; - for (let attackType in attackManager.startedAttacks) - { - for (let 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 != m.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) - { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer !== undefined) - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.Config.debug > 1) - API3.warn("No new target found. Remaining units " + this.unitCollection.length); - return false; - } - } - if (this.Config.debug > 1) - API3.warn("We will help one of our other attacks"); - } - this.targetPos = this.target.position(); - } - return true; -}; - -/** reset any units */ -m.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()) - { - let access = this.getAttackAccess(gameState); - let dist = Math.min(); - if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) - { - rallyPoint = this.rallyPoint; - dist = API3.SquareVectorDistance(this.position, rallyPoint); - } - // Then check if we have a nearer base (in case this attack has captured one) - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); - if (newdist > dist) - continue; - dist = newdist; - rallyPoint = base.anchor.position(); - } - } - - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - ent.stopMoving(); - if (rallyPoint) - ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); - this.removeUnit(ent); - } - } - - for (let unitCat in this.unitStat) - this.unit[unitCat].unregister(); - - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); -}; - -m.AttackPlan.prototype.removeUnit = function(ent, update) -{ - if (ent.getMetadata(PlayerID, "role") == "attack") - { - if (ent.hasClass("CitizenSoldier")) - ent.setMetadata(PlayerID, "role", "worker"); - else - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - } - ent.setMetadata(PlayerID, "plan", -1); - if (update) - this.unitCollection.updateEnt(ent); -}; - -m.AttackPlan.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.EntityRenamed) - { - if (!this.target || this.target.id() != evt.entity) - continue; - if (this.type == "Raid" && !this.isStarted()) - this.target = undefined; - else - this.target = gameState.getEntityById(evt.newentity); - if (this.target) - this.targetPos = this.target.position(); - } - - for (let evt of events.OwnershipChanged) // capture event - if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) - this.target = undefined; - - for (let evt of events.PlayerDefeated) - { - if (this.targetPlayer !== evt.playerId) - continue; - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - this.target = undefined; - } - - if (!this.overseas || this.state !== "unexecuted") - return; - // let's check if an enemy has built a structure at our access - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.position() || !ent.hasClass("Structure")) - continue; - if (!gameState.isPlayerEnemy(ent.owner())) - continue; - let access = m.getLandAccess(gameState, ent); - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - this.overseas = 0; - this.rallyPoint = base.anchor.position(); - } - } -}; - -m.AttackPlan.prototype.waitingForTransport = function() -{ - for (let ent of this.unitCollection.values()) - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return true; - return false; -}; - -m.AttackPlan.prototype.hasSiegeUnits = function() -{ - for (let ent of this.unitCollection.values()) - if (m.isSiegeUnit(ent)) - return true; - return false; -}; - -m.AttackPlan.prototype.hasForceOrder = function(data, value) -{ - for (let ent of this.unitCollection.values()) - { - if (data && +ent.getMetadata(PlayerID, data) !== value) - continue; - let orders = ent.unitAIOrderData(); - for (let order of orders) - if (order.force) - return true; - } - 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. - */ -m.AttackPlan.prototype.getAttackAccess = function(gameState) -{ - for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) - return m.getLandAccess(gameState, ent); - - return 0; -}; - -m.AttackPlan.prototype.debugAttack = function() -{ - API3.warn("---------- attack " + this.name); - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); - } - API3.warn("------------------------------"); -}; - -m.AttackPlan.prototype.Serialize = function() -{ - let properties = { - "name": this.name, - "type": this.type, - "state": this.state, - "forced": this.forced, - "rallyPoint": 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, - "isBlocked": this.isBlocked, - "targetPlayer": this.targetPlayer, - "target": this.target !== undefined ? this.target.id() : undefined, - "targetPos": this.targetPos, - "uniqueTargetId": this.uniqueTargetId, - "path": this.path - }; - - return { "properties": properties }; -}; - -m.AttackPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - if (this.target) - this.target = gameState.getEntityById(this.target); - - this.failed = undefined; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-imperialist/headquarters.js b/install/petraBased/petra-imperialist/headquarters.js deleted file mode 100644 index 60a49da..0000000 --- a/install/petraBased/petra-imperialist/headquarters.js +++ /dev/null @@ -1,2898 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Headquarters - * Deal with high level logic for the AI. Most of the interesting stuff gets done here. - * Some tasks: - * -defining RESS needs - * -BO decisions. - * > training workers - * > building stuff (though we'll send that to bases) - * -picking strategy (specific manager?) - * -diplomacy -> diplomacyManager - * -planning attacks -> attackManager - * -picking new CC locations. - */ - -m.HQ = function(Config) -{ - this.Config = Config; - this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i - - // Cache various quantities. - this.turnCache = {}; - this.lastFailedGather = {}; - - this.firstBaseConfig = false; - this.currentBase = 0; // Only one base (from baseManager) is run every turn. - - // Workers configuration - this.targetNumWorkers = this.Config.Economy.targetNumWorkers; - this.supportRatio = this.Config.Economy.supportRatio; - - this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime - this.towerStartTime = 0; // stone defense towers, will start as soon as available - this.towerLapseTime = this.Config.Military.towerLapseTime; - this.fortressStartTime = 0; // will start as soon as available - this.fortressLapseTime = this.Config.Military.fortressLapseTime; - this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); - this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); - - this.baseManagers = []; - this.attackManager = new m.AttackManager(this.Config); - this.buildManager = new m.BuildManager(); - this.defenseManager = new m.DefenseManager(this.Config); - this.tradeManager = new m.TradeManager(this.Config); - this.navalManager = new m.NavalManager(this.Config); - this.researchManager = new m.ResearchManager(this.Config); - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.garrisonManager = new m.GarrisonManager(this.Config); - this.victoryManager = new m.VictoryManager(this.Config); - - this.capturableTargets = new Map(); - this.capturableTargetsTime = 0; -}; - -/** More initialisation for stuff that needs the gameState */ -m.HQ.prototype.init = function(gameState, queues) -{ - this.territoryMap = m.createTerritoryMap(gameState); - // initialize base map. Each pixel is a base ID, or 0 if not or not accessible - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - // create borderMap: flag cells on the border of the map - // then this map will be completed with our frontier in updateTerritories - this.borderMap = m.createBorderMap(gameState); - // list of allowed regions - this.landRegions = {}; - // try to determine if we have a water map - this.navalMap = false; - this.navalRegions = {}; - - this.treasures = gameState.getEntities().filter(ent => { - let type = ent.resourceSupplyType(); - return type && type.generic == "treasure"; - }); - this.treasures.registerUpdates(); - this.currentPhase = gameState.currentPhase(); - this.decayingStructures = new Set(); -}; - -/** - * initialization needed after deserialization (only called when deserialization) - */ -m.HQ.prototype.postinit = function(gameState) -{ - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("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 - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - let base = this.getBaseByID(baseID); - base.assignResourceToDropsite(gameState, ent); - } - - this.updateTerritories(gameState); -}; - -/** - * 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 - * allowedType: undefined => new base with an anchor - * "unconstructed" => new base with a foundation anchor - * "captured" => captured base with an anchor - * "anchorless" => anchorless base, currently with dock - */ -m.HQ.prototype.createBase = function(gameState, ent, type) -{ - let access = m.getLandAccess(gameState, ent); - let newbase; - for (let base of this.baseManagers) - { - if (base.accessIndex != access) - continue; - if (type != "anchorless" && base.anchor) - continue; - if (type != "anchorless") - { - // TODO we keep the fisrt 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) - continue; - newbase = base; - if (newbase.anchor) - break; - } - } - - if (this.Config.debug > 0) - { - API3.warn(" ----------------------------------------------------------"); - API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); - API3.warn(" 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 m.BaseManager(gameState, this.Config); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type != "anchorless") - newbase.setAnchor(gameState, ent); - else - newbase.setAnchorlessEntity(gameState, ent); - - return newbase; -}; - -/** - * returns the sea index linking regions 1 and region 2 (supposed to be different land region) - * otherwise return undefined - * for the moment, only the case land-sea-land is supported - */ -m.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) -{ - let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); - if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") - return path[1]; - - if (this.Config.debug > 1) - { - API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); - API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); - API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); - } - return undefined; -}; - -/** TODO check if the new anchorless bases should be added to addBase */ -m.HQ.prototype.checkEvents = function(gameState, events) -{ - let addBase = false; - - this.buildManager.checkEvents(gameState, events); - - if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) - this.updateTerritories(gameState); - - for (let evt of events.DiplomacyChanged) - { - if (evt.player != PlayerID && evt.otherPlayer != PlayerID) - continue; - // Reset the entities collections which depend on diplomacy - gameState.resetOnDiplomacyChanged(); - break; - } - - for (let evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && - evt.metadata[PlayerID].base) - { - let ent = evt.entityObj; - if (ent.owner() != PlayerID) - continue; - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - let base = this.getBaseByID(evt.metadata[PlayerID].base); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState, ent); - } - } - - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (let evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - let newbase = this.createBase(gameState, ent, "unconstructed"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - let newbase = this.createBase(gameState, ent, "anchorless"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } - - for (let evt of events.ConstructionFinished) - { - if (evt.newentity == evt.entity) // repaired building - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID) - continue; - if (ent.hasClass("BarterMarket") && this.maxFields) - this.maxFields = false; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.from == PlayerID) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState, ent); - } - - if (evt.to != PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - if (ent.hasClass("Unit")) - { - m.getBestBase(gameState, ent).assignEntity(gameState, ent); - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - if (ent.hasClass("Trader")) - { - ent.setMetadata(PlayerID, "role", "trader"); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", "worker"); - ent.setMetadata(PlayerID, "subrole", "idle"); - } - if (ent.hasClass("Ship")) - m.setSeaAccess(gameState, ent); - if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, "unconstructed"); - else - { - newbase = this.createBase(gameState, ent, "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, "anchorless"); - else - base = m.getBestBase(gameState, ent) || this.baseManagers[0]; - base.assignEntity(gameState, ent); - if (ent.decaying()) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - } - } - - // deal with the different rally points of training units: the rally point is set when the training starts - // for the time being, only autogarrison is used - - for (let evt of events.TrainingStarted) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) - continue; - let metadata = ent._entity.trainingQueue[0].metadata; - if (metadata && metadata.garrisonType) - ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison - else - ent.unsetRallyPoint(); - } - - for (let evt of events.TrainingFinished) - { - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent.position()) - { - // we are autogarrisoned, check that the holder is registered in the garrisonManager - let holderId = ent.unitAIOrderData()[0].target; - let holder = gameState.getEntityById(holderId); - if (holder) - this.garrisonManager.registerHolder(gameState, holder); - } - else if (ent.getMetadata(PlayerID, "garrisonType")) - { - // we were supposed to be autogarrisoned, but this has failed (may-be full) - ent.setMetadata(PlayerID, "garrisonType", undefined); - } - - // Check if this unit is no more needed in its attack plan - // (happen when the training ends after the attack is started or aborted) - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = this.attackManager.getPlan(plan); - if (!attack || attack.state != "unexecuted") - ent.setMetadata(PlayerID, "plan", -1); - } - // Assign it immediately to something useful to do - if (ent.getMetadata(PlayerID, "role") == "worker") - { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) - { - base = m.getBestBase(gameState, ent); - base.assignEntity(gameState, ent); - } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); - } - else if (ent.resourceSupplyType() && ent.position()) - { - let type = ent.resourceSupplyType(); - if (!type.generic) - continue; - let dropsites = gameState.getOwnDropsites(type.generic); - let pos = ent.position(); - let access = m.getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (let dropsite of dropsites.values()) - { - if (!dropsite.position() || m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); - } - if (goal) - ent.moveToRange(goal[0], goal[1]); - } - } - } - - for (let evt of events.TerritoryDecayChanged) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) - continue; - if (evt.to) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - else if (ent.isGarrisonHolder()) - this.garrisonManager.removeDecayingStructure(evt.entity); - } - - if (addBase) - { - if (!this.firstBaseConfig) - { - // This is our first base, let us configure our starting resources - this.configFirstBase(gameState); - } - else - { - // Let us hope this new base will fix our possible resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } - } - - // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) - if (this.Config.difficulty < 2) - return; - for (let entId of this.decayingStructures) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.decaying() && ent.isOwn(PlayerID)) - { - let capture = ent.capturePoints(); - if (!capture) - continue; - let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); - if (captureRatio < 0.50) - continue; - let decayToGaia = true; - for (let i = 1; i < capture.length; ++i) - { - if (gameState.isPlayerAlly(i) || !capture[i]) - continue; - decayToGaia = false; - break; - } - if (decayToGaia) - continue; - let ratioMax = 0.70 + randFloat(0., 0.1); - for (let evt of events.Attacked) - { - if (ent.id() != evt.target) - continue; - ratioMax = 0.85 + randFloat(0., 0.1); - break; - } - if (captureRatio > ratioMax) - continue; - ent.destroy(); - } - this.decayingStructures.delete(entId); - } -}; - -/** Ensure that all requirements are met when phasing up*/ -m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) -{ - if (gameState.getNumberOfPhases() == this.currentPhase) - return; - - let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); - let plan; - let queue; - for (let entityReq of requirements) - { - // Village requirements are met elsewhere by constructing more houses - if (entityReq.class == "Village" || entityReq.class == "NotField") - continue; - if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) - continue; - switch (entityReq.class) - { - case "Town": - if (!queues.economicBuilding.hasQueuedUnits() && - !queues.militaryBuilding.hasQueuedUnits() && - !queues.defenseBuilding.hasQueuedUnits()) - { - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_market")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_market", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_temple")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_blacksmith")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "phaseUp": true }); - queue = "militaryBuilding"; - break; - } - if (this.canBuild(gameState, "structures/{civ}_defense_tower")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower", { "phaseUp": true }); - queue = "defenseBuilding"; - break; - } - } - break; - default: - // All classes not dealt with inside vanilla game. - // We put them for the time being on the economic queue, except if wonder - queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; - if (!queues[queue].hasQueuedUnits()) - { - let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); - if (structure && this.canBuild(gameState, structure)) - plan = new m.ConstructionPlan(gameState, structure, { "phaseUp": true }); - } - } - - 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); - return; - } - } -}; - -/** Called by any "phase" research plan once it's started */ -m.HQ.prototype.OnPhaseUp = function(gameState, phase) -{ -}; - -/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ -m.HQ.prototype.trainMoreWorkers = function(gameState, queues) -{ - // default template - let requirementsDef = [ ["costsResource", 1, "food"] ]; - let classesDef = ["Support", "Worker"]; - let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); - - // counting the workers that aren't part of a plan - let numberOfWorkers = 0; // all workers - let numberOfSupports = 0; // only support workers (i.e. non fighting) - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - { - ++numberOfWorkers; - if (ent.hasClass("Support")) - ++numberOfSupports; - } - }); - let numberInTraining = 0; - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - numberInTraining += item.count; - if (item.metadata && item.metadata.role && item.metadata.role == "worker" && - item.metadata.plan === undefined) - { - numberOfWorkers += item.count; - if (item.metadata.support) - numberOfSupports += item.count; - } - } - }); - - // Anticipate the optimal batch size when this queue will start - // and adapt the batch size of the first and second queued workers to the present population - // to ease a possible recovery if our population was drastically reduced by an attack - // (need to go up to second queued as it is accounted in queueManager) - let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); - if (queues.villager.plans[0]) - { - queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); - if (queues.villager.plans[1]) - queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); - } - if (queues.citizenSoldier.plans[0]) - { - queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); - if (queues.citizenSoldier.plans[1]) - queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); - } - - let numberOfQueuedSupports = queues.villager.countQueuedUnits(); - let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); - let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; - let numberTotal = numberOfWorkers + numberQueued; - - if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) - return; - if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && - this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) - return; - if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) - return; - - // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers - // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. - // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) - // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers - // and gives a ratio alfa at startup. - - let supportRatio = this.supportRatio; - let alpha = 0.85; - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field"))) - supportRatio = Math.min(this.supportRatio, 0.1); - if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) - alpha = 0.7; - if (gameState.isCeasefireActive()) - alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; - let supportMax = supportRatio * this.targetNumWorkers; - let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); - - let template; - if (!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] ]; - - let classes = ["CitizenSoldier", "Infantry"]; - // We want at least 33% ranged and 33% melee - classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); - - template = this.findBestTrainableUnit(gameState, classes, requirements); - } - - // If the template variable is empty, the default unit (Support unit) will be used - // base "0" means automatic choice of base - if (!template && templateDef) - queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); - else if (template) - queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); -}; - -/** picks the best template based on parameters and classes */ -m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) -{ - let units; - if (classes.indexOf("Hero") != -1) - units = gameState.findTrainableUnits(classes, []); - else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it - units = gameState.findTrainableUnits(classes, ["SiegeTower"]); - else // We do not want hero when not explicitely specified - units = gameState.findTrainableUnits(classes, ["Hero"]); - - if (!units.length) - return undefined; - - let parameters = requirements.slice(); - let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources - for (let type in remainingResources) - { - if (availableResources[type] > 800) - continue; - if (remainingResources[type] > 800) - continue; - let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; - let toAdd = true; - for (let param of parameters) - { - if (param[0] != "costsResource" || param[2] != type) - continue; - param[1] = Math.min(param[1], costsResource); - toAdd = false; - break; - } - if (toAdd) - parameters.push(["costsResource", costsResource, type]); - } - - units.sort((a, b) => { - let aCost = 1 + a[1].costSum(); - let bCost = 1 + b[1].costSum(); - let aValue = 0.1; - let bValue = 0.1; - for (let param of parameters) - { - if (param[0] == "strength") - { - aValue += m.getMaxStrength(a[1]) * param[1]; - bValue += m.getMaxStrength(b[1]) * param[1]; - } - else if (param[0] == "siegeStrength") - { - aValue += m.getMaxStrength(a[1], "Structure") * param[1]; - bValue += m.getMaxStrength(b[1], "Structure") * param[1]; - } - else if (param[0] == "speed") - { - aValue += a[1].walkSpeed() * param[1]; - bValue += b[1].walkSpeed() * param[1]; - } - else if (param[0] == "costsResource") - { - // requires a third parameter which is the resource - if (a[1].cost()[param[2]]) - aValue *= param[1]; - if (b[1].cost()[param[2]]) - bValue *= param[1]; - } - else if (param[0] == "canGather") - { - // checking against wood, could be anything else really. - if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) - aValue *= param[1]; - if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) - bValue *= param[1]; - } - else - API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); - } - return -aValue/aCost + bValue/bCost; - }); - return units[0][0]; -}; - -/** - * returns an entity collection of workers through BaseManager.pickBuilders - * TODO: when same accessIndex, sort by distance - */ -m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) -{ - let accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - // sorting bases by whether they are on the same accessindex or not. - let baseBest = this.baseManagers.slice().sort((a, b) => { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); - - let needed = number; - let workers = new API3.EntityCollection(gameState.sharedScript); - for (let base of baseBest) - { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) - break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; -}; - -m.HQ.prototype.getTotalResourceLevel = function(gameState) -{ - let total = {}; - for (let res of Resources.GetCodes()) - total[res] = 0; - for (let base of this.baseManagers) - for (let res in total) - total[res] += base.getResourceLevel(gameState, res); - - return total; -}; - -/** - * Returns the current gather rate - * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. - */ -m.HQ.prototype.GetCurrentGatherRates = function(gameState) -{ - if (!this.turnCache.currentRates) - { - let currentRates = {}; - for (let res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); - - for (let res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; -}; - -/** - * Returns the wanted gather rate. - */ -m.HQ.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. - */ -m.HQ.prototype.pickMostNeededResources = function(gameState) -{ - let wantedRates = this.GetWantedGatherRates(gameState); - let currentRates = this.GetCurrentGatherRates(gameState); - - let needed = []; - for (let res in wantedRates) - 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 Centre - * Whose primary function would be to reach new resources of type "resource". - */ -m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) -{ - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then look for a good spot. - - Engine.ProfileStart("findEconomicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); - let ccList = []; - for (let cc of ccEnts.values()) - ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); - let dpList = []; - for (let 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; - let nbShips = this.navalManager.transportShips.length; - if (proximity) // this is our first base - { - // if our first base, ensure room around - radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); - // scale is the typical scale at which we want to find a location for our first base - // look for bigger scale if we start from a ship (access < 2) or from a small island - let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; - proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); - if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) - scale = 400 * 400; - } - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). - let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; - let nearbyRejected = Math.square(120); // Reject if too near from any cc - let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc - let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc - let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc - let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc - let 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // We require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - if (proxyAccess && nbShips == 0 && proxyAccess != index) - continue; - - let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps - // Checking distance to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // 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 + API3.SquareVectorDistance(proximity, pos) / scale; - else - { - let minDist = Math.min(); - let accessible = false; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < nearbyRejected) - { - norm = 0; - break; - } - if (!cc.ally) - continue; - if (dist < nearbyAllyRejected) - { - norm = 0; - break; - } - if (dist < nearbyAllyDisfavored) - norm *= 0.5; - - if (dist < minDist) - minDist = dist; - accessible = accessible || index == m.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; - } - - // Not near any of our dropsite, except for oversea docks - oversea = !accessible && dpList.some(dp => m.getLandAccess(gameState, dp.ent) == index); - if (!oversea) - { - for (let dp of dpList) - { - let dist = API3.SquareVectorDistance(dp.pos, pos); - if (dist < 3600) - { - norm = 0; - break; - } - else if (dist < 6400) - norm *= 0.5; - } - } - if (norm == 0) - continue; - } - - if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map - norm *= 0.5; - - let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - val += gameState.sharedScript.ccResourceMaps[res].map[j]; - val *= norm; - - // If 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) - API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); - // not good enough. - if (bestVal < cut) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new Civil Centre - * Whose primary function would be to assure territorial continuity with our allies - */ -m.HQ.prototype.findStrategicCCLocation = function(gameState, template) -{ - // This builds a map. The procedure is fairly simple. - // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC - // with the constraints that all CC have dist > 200 and at least one have dist < 400 - // This needs at least 2 CC. Otherwise, go back to economic CC. - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let ccList = []; - let numAllyCC = 0; - for (let cc of ccEnts.values()) - { - let ally = gameState.isPlayerAlly(cc.owner()); - ccList.push({ "pos": cc.position(), "ally": ally }); - if (ally) - ++numAllyCC; - } - if (numAllyCC < 2) - return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); - - Engine.ProfileStart("findStrategicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestVal; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let currentVal, delta; - let distcc0, distcc1, distcc2; - let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // we require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - - // checking distances to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - let minDist = Math.min(); - distcc0 = undefined; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < 14000) // Reject if too near from any cc - { - minDist = 0; - break; - } - if (!cc.ally) - continue; - if (dist < 62000) // Reject if quite near from ally cc - { - minDist = 0; - break; - } - if (dist < minDist) - minDist = dist; - - if (!distcc0 || dist < distcc0) - { - distcc2 = distcc1; - distcc1 = distcc0; - distcc0 = dist; - } - else if (!distcc1 || dist < distcc1) - { - distcc2 = distcc1; - distcc1 = dist; - } - else if (!distcc2 || dist < distcc2) - distcc2 = dist; - } - if (minDist < 1 || minDist > 170000 && !this.navalMap) - continue; - - delta = Math.sqrt(distcc0) - favoredDistance; - currentVal = delta*delta; - delta = Math.sqrt(distcc1) - favoredDistance; - currentVal += delta*delta; - if (distcc2) - { - delta = Math.sqrt(distcc2) - favoredDistance; - currentVal += delta*delta; - } - // disfavor border of the map - if (this.borderMap.map[j] & m.fullBorder_Mask) - currentVal += 10000; - - if (bestVal !== undefined && currentVal > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = currentVal; - bestIdx = i; - } - - if (this.Config.debug > 1) - API3.warn("We've found a strategic base with bestVal = " + bestVal); - - Engine.ProfileStop(); - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new market: if the allies already have a market, build it as far as possible - * from it, although not in our border to be able to defend it easily. If no allied market, our second market will - * follow the same logic. - * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance - * for performance reasons. - */ -m.HQ.prototype.findMarketLocation = function(gameState, template) -{ - let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); - if (!markets.length) - markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); - - if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let bestDistSq; - let bestGainMult; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - let isNavalMarket = template.hasClass("NavalMarket"); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - // do not try on the narrow border of our territory - if (this.borderMap.map[j] & m.narrowFrontier_Mask) - continue; - if (this.basesMap.map[j] == 0) // only in our territory - continue; - // with enough room around to build the market - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other markets - let maxVal = 0; - let maxDistSq; - let maxGainMult; - let gainMultiplier; - for (let market of markets) - { - if (isNavalMarket && market.hasClass("NavalMarket")) - { - if (m.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) - continue; - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - } - else if (m.getLandAccess(gameState, market) == index && - !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else - continue; - if (!gainMultiplier) - continue; - let distSq = API3.SquareVectorDistance(market.position(), pos); - if (gainMultiplier * distSq > maxVal) - { - maxVal = gainMultiplier * distSq; - maxDistSq = distSq; - maxGainMult = gainMultiplier; - } - } - if (maxVal == 0) - continue; - if (bestVal !== undefined && maxVal < bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = maxVal; - bestDistSq = maxDistSq; - bestGainMult = maxGainMult; - bestIdx = i; - bestJdx = j; - } - - if (this.Config.debug > 1) - API3.warn("We found a market position with bestVal = " + bestVal); - - if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); - if (this.Config.debug > 1) - API3.warn("this would give a trading gain of " + expectedGain); - // do not keep it if gain is too small, except if this is our first BarterMarket - let idx; - if (expectedGain < this.tradeManager.minimalGain) - { - if (template.hasClass("BarterMarket") && - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - idx = -1; // needed by queueplanBuilding manager to keep that market - else - return false; - } - else - idx = this.basesMap.map[bestJdx]; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let 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 - */ -m.HQ.prototype.findDefensiveLocation = function(gameState, template) -{ - // We take the point in our territory which is the nearest to any enemy cc - // but requiring a minimal distance with our other defensive structures - // and not in range of any enemy defensive structure to avoid building under fire. - - let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); - let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals - { - enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) - enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) - return undefined; - } - enemyStructures = enemyStructures.toEntityArray(); - - let wonderMode = gameState.getVictoryConditions().has("wonder"); - let wonderDistmin; - let wonders; - if (wonderMode) - { - wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); - wonderMode = wonders.length != 0; - if (wonderMode) - wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); - } - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let isTower = template.hasClass("Tower"); - let isFortress = template.hasClass("Fortress"); - let radius; - if (isFortress) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (!wonderMode) - { - // do not try if well inside or outside territory - if (!(this.borderMap.map[j] & m.fullFrontier_Mask)) - continue; - if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower) - continue; - } - if (this.basesMap.map[j] == 0) // inaccessible cell - continue; - // with enough room around to build the cc - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other structures - let minDist = Math.min(); - - let dista = 0; - if (wonderMode) - { - dista = API3.SquareVectorDistance(wonders[0].position(), pos); - if (dista < wonderDistmin) - continue; - dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder - } - - for (let str of enemyStructures) - { - if (str.foundationProgress() !== undefined) - continue; - let strPos = str.position(); - if (!strPos) - continue; - let dist = API3.SquareVectorDistance(strPos, pos); - if (dist < 6400) // TODO check on true attack range instead of this 80*80 - { - minDist = -1; - break; - } - if (str.hasClass("CivCentre") && dist + dista < minDist) - minDist = dist + dista; - } - if (minDist < 0) - continue; - - let cutDist = 900; // 30*30 TODO maybe increase it - for (let str of ownStructures) - { - let strPos = str.position(); - if (!strPos) - continue; - if (API3.SquareVectorDistance(strPos, pos) < cutDist) - { - minDist = -1; - break; - } - } - if (minDist < 0 || minDist == Math.min()) - continue; - if (bestVal !== undefined && minDist > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = minDist; - bestIdx = i; - bestJdx = j; - } - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.basesMap.map[bestJdx]]; -}; - -m.HQ.prototype.buildTemple = function(gameState, queues) -{ - // at least one market (which have the same queue) should be build before any temple - if (queues.economicBuilding.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - // Try to build a temple earlier if in regicide to recruit healer guards - if (this.currentPhase < 3 && !gameState.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 m.ConstructionPlan(gameState, templateName)); -}; - -m.HQ.prototype.buildMarket = function(gameState, queues) -{ - if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_market")) - return; - - if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) - { - if (!queues.economicBuilding.paused) - { - // Put available resources in this market - let queueManager = gameState.ai.queueManager; - let cost = queues.economicBuilding.plans[0].getCost(); - queueManager.setAccounts(gameState, cost, "economicBuilding"); - if (!queueManager.canAfford("economicBuilding", cost)) - { - for (let q in queueManager.queues) - { - if (q == "economicBuilding") - continue; - queueManager.transferAccounts(cost, q, "economicBuilding"); - if (queueManager.canAfford("economicBuilding", cost)) - break; - } - } - } - return; - } - - gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -/** Build a farmstead */ -m.HQ.prototype.buildFarmstead = function(gameState, queues) -{ - // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) - if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) - return; - // Wait to have at least one dropsite and house before the farmstead - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) - return; - if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) - return; - if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) - return; - if (!this.canBuild(gameState, "structures/{civ}_farmstead")) - return; - - queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); -}; - -/** - * Try to build a wonder when required - * force = true when called from the victoryManager in case of Wonder victory condition. - */ -m.HQ.prototype.buildWonder = function(gameState, queues, force = false) -{ - if (queues.wonder && queues.wonder.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_wonder")) - return; - - if (!force) - { - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_wonder")); - // Check that we have enough resources to start thinking to build a wonder - let cost = template.cost(); - let resources = gameState.getResources(); - let highLevel = 0; - let lowLevel = 0; - for (let res in cost) - { - if (resources[res] && resources[res] > 0.7 * cost[res]) - ++highLevel; - else if (!resources[res] || resources[res] < 0.3 * cost[res]) - ++lowLevel; - } - if (highLevel == 0 || lowLevel > 1) - return; - } - - queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); -}; - -/** Build a corral, and train animals there */ -m.HQ.prototype.manageCorral = function(gameState, queues) -{ - if (queues.corral.hasQueuedUnits()) - return; - - let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; - if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) && - nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) - { - if (this.canBuild(gameState, "structures/{civ}_corral")) - { - queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); - return; - } - if (!nCorral) - return; - } - - // And train some animals - let civ = gameState.getPlayerCiv(); - for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) - { - if (corral.foundationProgress() !== undefined) - continue; - let trainables = corral.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.isHuntable()) - continue; - let count = gameState.countEntitiesByType(trainable, true); - for (let item of corral.trainingQueue()) - count += item.count; - if (count > nCorral) - continue; - queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); - return; - } - } -}; - -/** - * build more houses if needed. - * kinda ugly, lots of special cases to both build enough houses but not tooo many… - */ -m.HQ.prototype.buildMoreHouses = function(gameState, queues) -{ - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) || - gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return; - - let numPlanned = queues.house.length(); - if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) - { - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); - // change the starting condition according to the situation. - plan.goRequirement = "houseNeeded"; - queues.house.addPlan(plan); - } - - if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) - { - let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); - let houseTemplate = gameState.getTemplate(houseTemplateName); - - let needed = 0; - for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) - { - if (!houseTemplate.hasClass(entityReq.class)) - continue; - - let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; - if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to be less restrictive"); - this.buildManager.setBuildable(houseTemplateName); - this.requireHouses = true; - } - needed = Math.max(needed, entityReq.count - count); - } - - let houseQueue = queues.house.plans; - for (let i = 0; i < numPlanned; ++i) - if (houseQueue[i].isGo(gameState)) - --needed; - else if (needed > 0) - { - houseQueue[i].goRequirement = undefined; - --needed; - } - } - - if (this.requireHouses) - { - let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); - if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => - !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) - this.requireHouses = undefined; - } - - // When population limit too tight - // - if no room to build, try to improve with technology - // - otherwise increase temporarily the priority of houses - let house = gameState.applyCiv("structures/{civ}_house"); - let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; - let popBonus = gameState.getTemplate(house).getPopulationBonus(); - let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); - let priority; - if (freeSlots < 5) - { - if (this.buildManager.isUnbuildable(gameState, house)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - priority = 2*this.Config.priorities.house; - } - else - priority = this.Config.priorities.house; - - if (priority && priority != gameState.ai.queueManager.getPriority("house")) - gameState.ai.queueManager.changePriority("house", priority); -}; - -/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ -m.HQ.prototype.checkBaseExpansion = function(gameState, queues) -{ - if (queues.civilCentre.hasQueuedUnits()) - return; - // First build one cc if all have been destroyed - if (this.numPotentialBases() == 0) - { - this.buildFirstBase(gameState); - return; - } - // Then expand if we have not enough room available for buildings - if (this.buildManager.numberMissingRoom(gameState) > 1) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because not enough room to build "); - this.buildNewBase(gameState, queues); - return; - } - // If we've already planned to phase up, wait a bit before trying to expand - if (this.phasing) - return; - // Finally expand if we have lots of units (threshold depending on the aggressivity value) - let activeBases = this.numActiveBases(); - let numUnits = gameState.getOwnUnits().length; - let numvar = 10 * (1 - this.Config.personality.aggressive); - if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); - this.buildNewBase(gameState, queues); - } -}; - -m.HQ.prototype.buildNewBase = function(gameState, queues, resource) -{ - if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) - return false; - if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) - return false; - - let template; - // We require at least one of this civ civCentre as they may allow specific units or techs - let hasOwnCC = false; - for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) - { - if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}_civil_centre")) - continue; - hasOwnCC = true; - break; - } - if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else if (this.canBuild(gameState, "structures/{civ}_civil_centre")) - template = "structures/{civ}_civil_centre"; - else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else - return false; - - // base "-1" means new base. - if (this.Config.debug > 1) - API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); - queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); - return true; -}; - -/** Deals with building fortresses and towers along our border with enemies. */ -m.HQ.prototype.buildDefenses = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) - return; - - if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) - { - // try to build fortresses - if (this.canBuild(gameState, "structures/{civ}_fortress")) - { - let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; - if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && - numFortresses < this.numActiveBases() + 1 + this.extraFortresses && - numFortresses < Math.floor(gameState.getPopulation() / 25) && - gameState.getOwnFoundationsByClass("Fortress").length < 2) - { - this.fortressStartTime = gameState.ai.elapsedTime; - if (!numFortresses) - gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - return; - } - } - } - - if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) - { - let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers - let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime; - if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) - { - this.fortStartTime = gameState.ai.elapsedTime; - queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); - } - return; - } - - if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) - return; - - let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; - let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; - if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) && - numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && - numTowers < Math.floor(gameState.getPopulation() / 8) && - gameState.getOwnFoundationsByClass("DefenseTower").length < 3) - { - this.towerStartTime = gameState.ai.elapsedTime; - if (numTowers > 2 * this.numActiveBases() + 3) - gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - } -}; - -m.HQ.prototype.buildBlacksmith = function(gameState, queues) -{ - if (this.getAccountedPopulation(gameState) < this.Config.Military.popForBlacksmith || - queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) - return; - // build a market before the blacksmith - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - - if (this.canBuild(gameState, "structures/{civ}_blacksmith")) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); -}; - -/** - * Deals with constructing military buildings (barracks, stables…) - * They are mostly defined by Config.js. This is unreliable since changes could be done easily. - */ -m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) - return; - - let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; - if (this.saveResources && numBarracks != 0) - return; - - let barracksTemplate = this.canBuild(gameState, "structures/{civ}_barracks") ? "structures/{civ}_barracks" : undefined; - - let rangeTemplate = this.canBuild(gameState, "structures/{civ}_range") ? "structures/{civ}_range" : undefined; - let numRanges = gameState.getOwnEntitiesByClass("Archery", true).length; - numBarracks -= numRanges; - - let stableTemplate = this.canBuild(gameState, "structures/{civ}_stables") ? "structures/{civ}_stables" : - this.canBuild(gameState, "structures/{civ}_stable") ? "structures/{civ}_stable" : undefined; - let numStables = gameState.getOwnEntitiesByClass("Stables", true).length; - - if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || - this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) - { - // first barracks/range and stables. - if (numBarracks + numRanges == 0) - { - let template = barracksTemplate || rangeTemplate; - if (template) - { - gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); - let plan = new m.ConstructionPlan(gameState, template, { "militaryBase": true }); - plan.queueToReset = "militaryBuilding"; - queues.militaryBuilding.addPlan(plan); - return; - } - } - if (numStables == 0 && stableTemplate) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Second range/barracks and stables - if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Then 3rd barracks/range/stables if needed - if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) - { - let template = barracksTemplate || stableTemplate || rangeTemplate; - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - } - - if (this.saveResources) - return; - - if (this.currentPhase < 3) - return; - - if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true })); - return; - } - - if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true })); - return; - } - - if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) - return; - - // Build advanced military buildings - let nAdvanced = 0; - for (let advanced of this.bAdvanced) - nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); - - if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) - { - for (let advanced of this.bAdvanced) - { - if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) - continue; - let template = gameState.getTemplate(advanced); - if (!template) - continue; - let civ = gameState.getPlayerCiv(); - if (template.hasDefensiveFire() || template.trainableEntities(civ)) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true })); - else // not a military building, but still use this queue - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); - return; - } - } -}; - -/** - * Find base nearest to ennemies for military buildings. - */ -m.HQ.prototype.findBestBaseForMilitary = function(gameState) -{ - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); - let bestBase; - let enemyFound = false; - let distMin = Math.min(); - for (let cce of ccEnts) - { - if (gameState.isPlayerAlly(cce.owner())) - continue; - if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) - continue; - let access = m.getLandAccess(gameState, cce); - let isEnemy = gameState.isPlayerEnemy(cce.owner()); - for (let cc of ccEnts) - { - if (cc.owner() != PlayerID) - continue; - if (m.getLandAccess(gameState, cc) != access) - continue; - let dist = API3.SquareVectorDistance(cc.position(), cce.position()); - if (!enemyFound && isEnemy) - enemyFound = true; - else if (dist > distMin) - continue; - bestBase = cc.getMetadata(PlayerID, "base"); - distMin = dist; - } - } - return bestBase; -}; - -/** - * train with highest priority ranged infantry in the nearest civil centre from a given set of positions - * and garrison them there for defense - */ -m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) -{ - if (gameState.ai.queues.emergency.hasQueuedUnits()) - return false; - - let civ = gameState.getPlayerCiv(); - // find nearest base anchor - let distcut = 20000; - let nearestAnchor; - let distmin; - for (let pos of positions) - { - let access = gameState.ai.accessibility.getAccessValue(pos); - // check nearest base anchor - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - if (!base.anchor.trainableEntities(civ)) // base still in construction - continue; - let queue = base.anchor._entity.trainingQueue; - if (queue) - { - let time = 0; - for (let item of queue) - if (item.progress > 0 || item.metadata && item.metadata.garrisonType) - time += item.timeRemaining; - if (time/1000 > 5) - continue; - } - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (nearestAnchor && dist > distmin) - continue; - distmin = dist; - nearestAnchor = base.anchor; - } - } - if (!nearestAnchor || distmin > distcut) - return false; - - // We will choose randomly ranged and melee units, except when garrisonHolder is full - // in which case we prefer melee units - let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); - if (nearestAnchor._entity.trainingQueue) - { - for (let item of nearestAnchor._entity.trainingQueue) - { - if (item.metadata && item.metadata.garrisonType) - numGarrisoned += item.count; - else if (!item.progress && (!item.metadata || !item.metadata.trainer)) - nearestAnchor.stopProduction(item.id); - } - } - let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && - nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); - let rangedWanted = randBool() && autogarrison; - - let total = gameState.getResources(); - let templateFound; - let trainables = nearestAnchor.trainableEntities(civ); - let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) - continue; - if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) - continue; - if (!total.canAfford(new API3.Resources(template.cost()))) - continue; - templateFound = [trainable, template]; - if (template.hasClass("Ranged") == rangedWanted) - break; - } - if (!templateFound) - return false; - - // Check first if we can afford it without touching the other accounts - // and if not, take some of other accounted resources - // TODO sort the queues to be substracted - let queueManager = gameState.ai.queueManager; - let cost = new API3.Resources(templateFound[1].cost()); - queueManager.setAccounts(gameState, cost, "emergency"); - if (!queueManager.canAfford("emergency", cost)) - { - for (let q in queueManager.queues) - { - if (q == "emergency") - continue; - queueManager.transferAccounts(cost, q, "emergency"); - if (queueManager.canAfford("emergency", cost)) - break; - } - } - let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; - if (autogarrison) - metadata.garrisonType = "protection"; - gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); - return true; -}; - -m.HQ.prototype.canBuild = function(gameState, structure) -{ - let type = gameState.applyCiv(structure); - if (this.buildManager.isUnbuildable(gameState, type)) - return false; - - if (gameState.isTemplateDisabled(type)) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); - return false; - } - - let template = gameState.getTemplate(type); - if (!template) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); - return false; - } - - if (!template.available(gameState)) - { - this.buildManager.setUnbuildable(gameState, type, 30, "tech"); - return false; - } - - if (!this.buildManager.hasBuilder(type)) - { - this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); - return false; - } - - if (this.numActiveBases() < 1) - { - // if no base, check that we can build outside our territory - let buildTerritories = template.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) - { - this.buildManager.setUnbuildable(gameState, type, 180, "room"); - return false; - } - } - - // build limits - let limits = gameState.getEntityLimits(); - let category = template.buildCategory(); - if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) - { - this.buildManager.setUnbuildable(gameState, type, 90, "limit"); - return false; - } - - return true; -}; - -m.HQ.prototype.updateTerritories = function(gameState) -{ - const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; - let alliedVictory = gameState.getAlliedVictory(); - let passabilityMap = gameState.getPassabilityMap(); - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let insideSmall = Math.round(45 / cellSize); - let insideLarge = Math.round(80 / cellSize); // should be about the range of towers - let expansion = 0; - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.borderMap.map[j] & m.outside_Mask) - continue; - if (this.borderMap.map[j] & m.fullFrontier_Mask) - this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier - - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - // If this tile was already accounted, remove it - if (this.basesMap.map[j] == 0) - continue; - let base = this.getBaseByID(this.basesMap.map[j]); - if (base) - { - let index = base.territoryIndices.indexOf(j); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); - } - else - API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); - this.basesMap.map[j] = 0; - } - else - { - // Update the frontier - let ix = j%width; - let iz = Math.floor(j/width); - let onFrontier = false; - for (let a of around) - { - let jx = ix + Math.round(insideSmall*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(insideSmall*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - { - this.borderMap.map[j] |= m.narrowFrontier_Mask; - break; - } - jx = ix + Math.round(insideLarge*a[0]); - if (jx < 0 || jx >= width) - continue; - jz = iz + Math.round(insideLarge*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - onFrontier = true; - } - if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask)) - this.borderMap.map[j] |= m.largeFrontier_Mask; - - // If this tile was not already accounted, add it. - if (this.basesMap.map[j] != 0) - continue; - let landPassable = false; - let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); - let access; - for (let k of ind) - { - if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - continue; - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - continue; - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - expansion++; - } - } - - if (!expansion) - return; - // We've increased our territory, so we may have some new room to build - this.buildManager.resetMissingRoom(gameState); - // And if sufficient expansion, check if building a new market would improve our present trade routes - let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; - if (expansion * cellArea > 960) - this.tradeManager.routeProspection = true; -}; - -/** Reassign territories when a base is going to be deleted */ -m.HQ.prototype.reassignTerritories = function(deletedBase) -{ - let cellSize = this.territoryMap.cellSize; - let width = this.territoryMap.width; - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != deletedBase.accessIndex) - continue; - let dist = API3.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; - } -}; - -/** - * returns the base corresponding to baseID - */ -m.HQ.prototype.getBaseByID = function(baseID) -{ - for (let base of this.baseManagers) - if (base.ID == baseID) - return base; - - return undefined; -}; - -/** - * returns the number of bases with a cc - * ActiveBases includes only those with a built cc - * PotentialBases includes also those with a cc in construction - */ -m.HQ.prototype.numActiveBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; -}; - -m.HQ.prototype.numPotentialBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -m.HQ.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (let base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } -}; - -m.HQ.prototype.resetBaseCache = function() -{ - this.turnCache.base = undefined; -}; - -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). - */ -m.HQ.prototype.assignGatherers = function() -{ - for (let base of this.baseManagers) - { - for (let worker of base.workers.values()) - { - if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE") - continue; - let orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); - } - } -}; - -m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) -{ - return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); -}; - -/** Check that the chosen position is not too near from an invading army */ -m.HQ.prototype.isNearInvadingArmy = function(pos) -{ - for (let army of this.defenseManager.armies) - if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) - return true; - return false; -}; - -m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) -{ - if (!this.turnCache.firingStructures) - this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); - for (let ent of this.turnCache.firingStructures.values()) - { - let range = radius + ent.attackRange("Ranged").max; - if (API3.SquareVectorDistance(ent.position(), pos) < range*range) - return true; - } - return false; -}; - -/** Compute the capture strength of all units attacking a capturable target */ -m.HQ.prototype.updateCaptureStrength = function(gameState) -{ - this.capturableTargets.clear(); - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.canCapture()) - continue; - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || !target.isCapturable() || !ent.canCapture(target)) - continue; - if (!this.capturableTargets.has(targetId)) - this.capturableTargets.set(targetId, { - "strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"), - "ents": new Set([ent.id()]) - }); - else - { - let capturableTarget = this.capturableTargets.get(target.id()); - capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTarget.ents.add(ent.id()); - } - } - - for (let [targetId, capturableTarget] of this.capturableTargets) - { - let target = gameState.getEntityById(targetId); - let allowCapture; - for (let entId of capturableTarget.ents) - { - let ent = gameState.getEntityById(entId); - if (allowCapture === undefined) - allowCapture = m.allowCapture(gameState, ent, target); - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].attackType) - continue; - if ((orderData[0].attackType == "Capture") !== allowCapture) - ent.attack(targetId, allowCapture); - } - } - - this.capturableTargetsTime = gameState.ai.elapsedTime; -}; - -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** add a gatherer to the turn cache for this supply. */ -m.HQ.prototype.AddTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) - ++this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = 1; - } -}; - -/** remove a gatherer to the turn cache for this supply. */ -m.HQ.prototype.RemoveTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - --this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = -1; - } -}; - -m.HQ.prototype.GetTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - return this.turnCache.resourceGatherer[supplyID]; - - return 0; -}; - -/** The next two are to register that we assigned a gatherer to a resource this turn. */ -m.HQ.prototype.AddTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - ++this.turnCache["resourceGatherer-" + resource]; - else - this.turnCache["resourceGatherer-" + resource] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -m.HQ.prototype.GetTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - return this.turnCache["resourceGatherer-" + resource]; - - return 0; -}; - -/** - * flag a resource as exhausted - */ -m.HQ.prototype.isResourceExhausted = function(resource) -{ - if (this.turnCache["exhausted-" + resource] == undefined) - this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); - - return this.turnCache["exhausted-" + resource]; -}; - -/** - * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) - */ -m.HQ.prototype.isDefendable = function(ent) -{ - if (!this.turnCache.numAround) - this.turnCache.numAround = {}; - if (this.turnCache.numAround[ent.id()] === undefined) - this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); - return +this.turnCache.numAround[ent.id()] > 8; -}; - -/** - * Get the number of population already accounted for - */ -m.HQ.prototype.getAccountedPopulation = function(gameState) -{ - if (this.turnCache.accountedPopulation == undefined) - { - let pop = gameState.getPopulation(); - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.unitTemplate) - continue; - let 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 - */ -m.HQ.prototype.getAccountedWorkers = function(gameState) -{ - if (this.turnCache.accountedWorkers == undefined) - { - let workers = gameState.getOwnEntitiesByRole("worker", true).length; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") - continue; - workers += item.count; - } - } - this.turnCache.accountedWorkers = workers; - } - return this.turnCache.accountedWorkers; -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.HQ.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Headquarters update"); - this.turnCache = {}; - this.territoryMap = m.createTerritoryMap(gameState); - this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities(); - // TODO find a better way to update - if (this.currentPhase != gameState.currentPhase()) - { - if (this.Config.debug > 0) - API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + - " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + - " phasing " + this.phasing); - this.currentPhase = gameState.currentPhase(); - - // In principle, this.phasing should be already reset to 0 when starting the research - // but this does not work in case of an autoResearch tech - if (this.phasing) - this.phasing = 0; - } - -/* if (this.Config.debug > 1) - { - gameState.getOwnUnits().forEach (function (ent) { - if (!ent.position()) - return; - m.dumpEntity(ent); - }); - } */ - - this.checkEvents(gameState, events); - this.navalManager.checkEvents(gameState, queues, events); - - if (this.phasing) - this.checkPhaseRequirements(gameState, queues); - else - this.researchManager.checkPhase(gameState, queues); - - if (this.numActiveBases() > 0) - { - if (gameState.ai.playedTurn % 4 == 0) - this.trainMoreWorkers(gameState, queues); - - if (gameState.ai.playedTurn % 4 == 1) - this.buildMoreHouses(gameState, queues); - - if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) - this.buildFarmstead(gameState, queues); - - if (this.needCorral && gameState.ai.playedTurn % 4 == 3) - this.manageCorral(gameState, queues); - - if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) - this.researchManager.update(gameState, queues); - } - - if (this.numPotentialBases() < 1 || - this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) - this.checkBaseExpansion(gameState, queues); - - if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) - { - if (!this.canBarter) - this.buildMarket(gameState, queues); - - if (!this.saveResources) - { - this.buildBlacksmith(gameState, queues); - this.buildTemple(gameState, queues); - } - - if (gameState.ai.playedTurn % 30 == 0 && - gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) - this.buildWonder(gameState, queues, false); - } - - this.tradeManager.update(gameState, events, queues); - - this.garrisonManager.update(gameState, events); - this.defenseManager.update(gameState, events); - - if (gameState.ai.playedTurn % 3 == 0) - { - this.constructTrainingBuildings(gameState, queues); - if (this.Config.difficulty > 0) - this.buildDefenses(gameState, queues); - } - - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase; // We will loop only on 1 active base per turn - do - { - 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]); - } - while (!activeBase && nbBases != 0); - - this.navalManager.update(gameState, queues, events); - - if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) - this.attackManager.update(gameState, queues, events); - - this.diplomacyManager.update(gameState, events); - - this.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(); -}; - -m.HQ.prototype.Serialize = function() -{ - let properties = { - "phasing": this.phasing, - "currentBase": this.currentBase, - "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 - }; - - let baseManagers = []; - for (let base of this.baseManagers) - baseManagers.push(base.Serialize()); - - if (this.Config.debug == -100) - { - API3.warn(" HQ serialization ---------------------"); - API3.warn(" properties " + uneval(properties)); - API3.warn(" baseManagers " + uneval(baseManagers)); - API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); - API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); - API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); - API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); - API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); - API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); - API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); - API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); - API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); - } - - return { - "properties": properties, - - "baseManagers": baseManagers, - "attackManager": this.attackManager.Serialize(), - "buildManager": this.buildManager.Serialize(), - "defenseManager": this.defenseManager.Serialize(), - "tradeManager": this.tradeManager.Serialize(), - "navalManager": this.navalManager.Serialize(), - "researchManager": this.researchManager.Serialize(), - "diplomacyManager": this.diplomacyManager.Serialize(), - "garrisonManager": this.garrisonManager.Serialize(), - "victoryManager": this.victoryManager.Serialize(), - }; -}; - -m.HQ.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.baseManagers = []; - for (let base of data.baseManagers) - { - // the first call to deserialize set the ID base needed by entitycollections - let newbase = new m.BaseManager(gameState, this.Config); - newbase.Deserialize(gameState, base); - newbase.init(gameState); - newbase.Deserialize(gameState, base); - this.baseManagers.push(newbase); - } - - this.navalManager = new m.NavalManager(this.Config); - this.navalManager.init(gameState, true); - this.navalManager.Deserialize(gameState, data.navalManager); - - this.attackManager = new m.AttackManager(this.Config); - this.attackManager.Deserialize(gameState, data.attackManager); - this.attackManager.init(gameState); - this.attackManager.Deserialize(gameState, data.attackManager); - - this.buildManager = new m.BuildManager(); - this.buildManager.Deserialize(data.buildManager); - - this.defenseManager = new m.DefenseManager(this.Config); - this.defenseManager.Deserialize(gameState, data.defenseManager); - - this.tradeManager = new m.TradeManager(this.Config); - this.tradeManager.init(gameState); - this.tradeManager.Deserialize(gameState, data.tradeManager); - - this.researchManager = new m.ResearchManager(this.Config); - this.researchManager.Deserialize(data.researchManager); - - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.diplomacyManager.Deserialize(data.diplomacyManager); - - this.garrisonManager = new m.GarrisonManager(this.Config); - this.garrisonManager.Deserialize(data.garrisonManager); - - this.victoryManager = new m.VictoryManager(this.Config); - this.victoryManager.Deserialize(data.victoryManager); -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-imperialist/queueManager.js b/install/petraBased/petra-imperialist/queueManager.js deleted file mode 100644 index fa74b8d..0000000 --- a/install/petraBased/petra-imperialist/queueManager.js +++ /dev/null @@ -1,542 +0,0 @@ -var PETRA = function(m) -{ - -// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. -// -// Currently this manager keeps accounts for each queue, split between the 4 main resources -// -// Each time resources are available (ie not in any account), it is split between the different queues -// Mostly based on priority of the queue, and existing needs. -// Each turn, the queue Manager checks if a queue can afford its next item, then it does. -// -// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it -// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will -// be able to benefit form the 500 food (even if they only needed food). -// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. -// -// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues -// get some part of the total, and if all queues have 70% of their needs, nothing gets done -// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. -// -// This system should be improved. It's probably not flexible enough. - -m.QueueManager = function(Config, queues) -{ - this.Config = Config; - this.queues = queues; - this.priorities = {}; - for (let i in Config.priorities) - this.priorities[i] = Config.priorities[i]; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in this.queues) - { - this.accounts[q] = new API3.Resources(); - this.queueArrays.push([q, this.queues[q]]); - } - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getAvailableResources = function(gameState) -{ - let resources = gameState.getResources(); - for (let key in this.queues) - resources.subtract(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.getTotalAccountedResources = function() -{ - let resources = new API3.Resources(); - for (let key in this.queues) - resources.add(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.currentNeeds = function(gameState) -{ - let needed = new API3.Resources(); - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) - continue; - let costs = queue.plans[0].getCost(); - needed.add(costs); - } - // get out current resources, not removing accounts. - let current = gameState.getResources(); - for (let res of Resources.GetCodes()) - needed[res] = Math.max(0, needed[res] - current[res]); - - return needed; -}; - -// calculate the gather rates we'd want to be able to start all elements in our queues -// TODO: many things. -m.QueueManager.prototype.wantedGatherRates = function(gameState) -{ - // default values for first turn when we have not yet set our queues. - if (gameState.ai.playedTurn === 0) - { - let ret = {}; - for (let res of Resources.GetCodes()) - ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; - return ret; - } - - // get out current resources, not removing accounts. - let current = gameState.getResources(); - // short queue is the first item of a queue, assumed to be ready in 30s - // medium queue is the second item of a queue, assumed to be ready in 60s - // long queue contains the isGo=false items, assumed to be ready in 300s - let totalShort = {}; - let totalMedium = {}; - let totalLong = {}; - for (let res of Resources.GetCodes()) - { - totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; - totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; - totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; - } - let total; - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (queue.paused) - continue; - for (let j = 0; j < queue.length(); ++j) - { - if (j > 1) - break; - let cost = queue.plans[j].getCost(); - if (queue.plans[j].isGo(gameState)) - { - if (j === 0) - total = totalShort; - else - total = totalMedium; - } - else - total = totalLong; - for (let type in total) - total[type] += cost[type]; - if (!queue.plans[j].isGo(gameState)) - break; - } - } - // global rates - let rates = {}; - let diff; - for (let res of Resources.GetCodes()) - { - if (current[res] > 0) - { - diff = Math.min(current[res], totalShort[res]); - totalShort[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - { - diff = Math.min(current[res], totalMedium[res]); - totalMedium[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - totalLong[res] -= Math.min(current[res], totalLong[res]); - } - } - rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; - } - - return rates; -}; - -m.QueueManager.prototype.printQueues = function(gameState) -{ - let numWorkers = 0; - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - numWorkers++; - }); - API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); - for (let i in this.queues) - { - let q = this.queues[i]; - if (q.hasQueuedUnits()) - { - API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); - API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); - } - for (let plan of q.plans) - { - let qStr = " " + plan.type + " "; - if (plan.number) - qStr += "x" + plan.number; - qStr += " isGo " + plan.isGo(gameState); - API3.warn(qStr); - } - } - API3.warn("Accounts"); - for (let p in this.accounts) - API3.warn(p + ": " + uneval(this.accounts[p])); - API3.warn("Current Resources: " + uneval(gameState.getResources())); - API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); - API3.warn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); - API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); - API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); - API3.warn("------------------------------------"); -}; - -m.QueueManager.prototype.clear = function() -{ - for (let i in this.queues) - this.queues[i].empty(); -}; - -/** - * set accounts of queue i from the unaccounted resources - */ -m.QueueManager.prototype.setAccounts = function(gameState, cost, i) -{ - let available = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (this.accounts[i][res] >= cost[res]) - continue; - this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); - } -}; - -/** - * transfer accounts from queue i to queue j - */ -m.QueueManager.prototype.transferAccounts = function(cost, i, j) -{ - for (let res of Resources.GetCodes()) - { - if (this.accounts[j][res] >= cost[res]) - continue; - let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); - this.accounts[i][res] -= diff; - this.accounts[j][res] += diff; - } -}; - -/** - * distribute the resources between the different queues according to their priorities - */ -m.QueueManager.prototype.distributeResources = function(gameState) -{ - let availableRes = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) - { - let total = gameState.getResources()[res]; - let scale = total / (total - availableRes[res]); - availableRes[res] = total; - for (let j in this.queues) - { - this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); - availableRes[res] -= this.accounts[j][res]; - } - } - - if (!availableRes[res]) - { - this.switchResource(gameState, res); - continue; - } - - for (let q in this.queues) { - let queueCost = this.queues[q].maxAccountWanted(gameState, 0); - if (this.queues[q].hasQueuedUnits() && this.accounts[q][res] < queueCost[res] && !this.queues[q].paused) { - this.accounts[q][res] = queueCost[res]; - } - } - } -}; - -m.QueueManager.prototype.switchResource = function(gameState, res) -{ - // We have no available resources, see if we can't "compact" them in one queue. - // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. - // TODO: this isn't perfect compression. - for (let j in this.queues) - { - if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) - continue; - - let queue = this.queues[j]; - let queueCost = queue.maxAccountWanted(gameState, 0); - if (this.accounts[j][res] >= queueCost[res]) - continue; - - for (let i in this.queues) - { - if (i === j) - continue; - let otherQueue = this.queues[i]; - if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) - continue; - if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) - continue; - - let diff = queueCost[res] - this.accounts[j][res]; - this.accounts[j][res] += diff; - this.accounts[i][res] -= diff; - ++otherQueue.switched; - if (this.Config.debug > 2) - API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); - break; - } - } -}; - -// Start the next item in the queue if we can afford it. -m.QueueManager.prototype.startNextItems = function(gameState) -{ - for (let q of this.queueArrays) - { - let name = q[0]; - let queue = q[1]; - if (queue.hasQueuedUnits() && !queue.paused) - { - let item = queue.getNext(); - if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) - { - // canStart may update the cost because of the costMultiplier so we must check it again - if (this.accounts[name].canAfford(item.getCost())) - { - this.finishingTime = gameState.ai.elapsedTime; - this.accounts[name].subtract(item.getCost()); - queue.startNext(gameState); - queue.switched = 0; - } - } - } - else if (!queue.hasQueuedUnits()) - { - this.accounts[name].reset(); - queue.switched = 0; - } - } -}; - -m.QueueManager.prototype.update = function(gameState) -{ - Engine.ProfileStart("Queue Manager"); - - for (let i in this.queues) - { - this.queues[i].check(gameState); // do basic sanity checks on the queue - if (this.priorities[i] > 0) - continue; - API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); - this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. - } - - // Pause or unpause queues depending on the situation - this.checkPausedQueues(gameState); - - // Let's assign resources to plans that need them - this.distributeResources(gameState); - - // Start the next item in the queue if we can afford it. - this.startNextItems(gameState); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - this.printQueues(gameState); - - Engine.ProfileStop(); -}; - -// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training -m.QueueManager.prototype.checkPausedQueues = function(gameState) -{ - let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); - let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); - for (let q in this.queues) - { - let toBePaused = false; - if (gameState.ai.HQ.numPotentialBases() == 0) - toBePaused = q != "dock" && q != "civilCentre"; - else if (numWorkers < workersMin / 3) - toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; - else if (numWorkers < workersMin * 2 / 3) - toBePaused = q == "civilCentre" || q == "economicBuilding" || - q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || - q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; - else if (numWorkers < workersMin) - toBePaused = q == "civilCentre" || q == "defenseBuilding" || - q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; - - if (toBePaused) - { - if (q == "field" && gameState.ai.HQ.needFarm && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "corral" && gameState.ai.HQ.needCorral && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "dock" && gameState.ai.HQ.needFish && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) - toBePaused = false; - if (q == "ships" && gameState.ai.HQ.needFish && - !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) - toBePaused = false; - } - - let queue = this.queues[q]; - if (!queue.paused && toBePaused) - { - queue.paused = true; - this.accounts[q].reset(); - } - else if (queue.paused && !toBePaused) - queue.paused = false; - - // And reduce the batch sizes of attack queues - if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) - { - queue.plans[0].number = 1; - if (queue.plans[1]) - queue.plans[1].number = 1; - } - } -}; - -m.QueueManager.prototype.canAfford = function(queue, cost) -{ - if (!this.accounts[queue]) - return false; - return this.accounts[queue].canAfford(cost); -}; - -m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) -{ - if (!this.queues[queue]) - return; - this.queues[queue].paused = true; - if (scrapAccounts) - this.accounts[queue].reset(); -}; - -m.QueueManager.prototype.unpauseQueue = function(queue) -{ - if (this.queues[queue]) - this.queues[queue].paused = false; -}; - -m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) -{ - for (let q in this.queues) - { - if (q == but) - continue; - if (scrapAccounts) - this.accounts[q].reset(); - this.queues[q].paused = true; - } -}; - -m.QueueManager.prototype.unpauseAll = function(but) -{ - for (let q in this.queues) - if (q != but) - this.queues[q].paused = false; -}; - - -m.QueueManager.prototype.addQueue = function(queueName, priority) -{ - if (this.queues[queueName] !== undefined) - return; - - this.queues[queueName] = new m.Queue(); - this.priorities[queueName] = priority; - this.accounts[queueName] = new API3.Resources(); - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.removeQueue = function(queueName) -{ - if (this.queues[queueName] === undefined) - return; - - delete this.queues[queueName]; - delete this.priorities[queueName]; - delete this.accounts[queueName]; - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getPriority = function(queueName) -{ - return this.priorities[queueName]; -}; - -m.QueueManager.prototype.changePriority = function(queueName, newPriority) -{ - if (this.Config.debug > 1) - API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); - if (this.queues[queueName] !== undefined) - this.priorities[queueName] = newPriority; - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.Serialize = function() -{ - let accounts = {}; - let queues = {}; - for (let q in this.queues) - { - queues[q] = this.queues[q].Serialize(); - accounts[q] = this.accounts[q].Serialize(); - if (this.Config.debug == -100) - API3.warn("queueManager serialization: queue " + q + " >>> " + - uneval(queues[q]) + " with accounts " + uneval(accounts[q])); - } - - return { - "priorities": this.priorities, - "queues": queues, - "accounts": accounts - }; -}; - -m.QueueManager.prototype.Deserialize = function(gameState, data) -{ - this.priorities = data.priorities; - this.queues = {}; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in data.queues) - { - this.queues[q] = new m.Queue(); - this.queues[q].Deserialize(gameState, data.queues[q]); - this.accounts[q] = new API3.Resources(); - this.accounts[q].Deserialize(data.accounts[q]); - this.queueArrays.push([q, this.queues[q]]); - } - this.queueArrays.sort((a, b) => data.priorities[b[0]] - data.priorities[a[0]]); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/attackManager.js b/install/petraBased/petra-patriot/attackManager.js deleted file mode 100644 index ebccbee..0000000 --- a/install/petraBased/petra-patriot/attackManager.js +++ /dev/null @@ -1,805 +0,0 @@ -var PETRA = function(m) -{ - -/** Attack Manager */ - -m.AttackManager = function(Config) -{ - this.Config = Config; - - this.totalNumber = 0; - this.attackNumber = 0; - this.rushNumber = 0; - this.raidNumber = 0; - this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.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 = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.AttackManager.prototype.init = function(gameState) -{ - this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); - this.outOfPlan.registerUpdates(); -}; - -m.AttackManager.prototype.setRushes = function(allowed) -{ - if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) - { - this.maxRushes = 3; - this.rushSize = [ 16, 20, 24 ]; - } - 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 ]; - } -}; - -m.AttackManager.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.PlayerDefeated) - this.defeated[evt.playerId] = true; - - let answer = "decline"; - let other; - let targetPlayer; - for (let evt of events.AttackRequest) - { - if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) - continue; - targetPlayer = evt.player; - let available = 0; - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing") - { - if (attack.targetPlayer === targetPlayer) - available += attack.unitCollection.length; - else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) - other = attack.targetPlayer; - continue; - } - - attack.targetPlayer = targetPlayer; - - if (attack.unitCollection.length > 2) - available += attack.unitCollection.length; - } - } - - if (available > 12) // launch the attack immediately - { - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing" || - attack.targetPlayer !== targetPlayer || - attack.unitCollection.length < 3) - continue; - attack.forceStart(); - attack.requested = true; - } - } - answer = "join"; - } - else if (other !== undefined) - answer = "other"; - break; // take only the first attack request into account - } - if (targetPlayer !== undefined) - m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); - - for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks - { - for (let [targetId, unitIds] of this.bombingAttacks) - { - 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 - */ -m.AttackManager.prototype.assignBombers = function(gameState) -{ - // First some cleaning of current bombing attacks - for (let [targetId, unitIds] of this.bombingAttacks) - { - let target = gameState.getEntityById(targetId); - if (!target || !gameState.isPlayerEnemy(target.owner())) - this.bombingAttacks.delete(targetId); - else - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.owner() == PlayerID) - { - let plan = ent.getMetadata(PlayerID, "plan"); - let orders = ent.unitAIOrderData(); - let 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); - } - } - - let bombers = gameState.updatingCollection("bombers", API3.Filters.byClassesOr(["BoltShooter", "Catapult"]), gameState.getOwnUnits()); - for (let 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - continue; - } - let alreadyBombing = false; - for (let unitIds of this.bombingAttacks.values()) - { - if (!unitIds.has(ent.id())) - continue; - alreadyBombing = true; - break; - } - if (alreadyBombing) - break; - - let range = ent.attackRange("Ranged").max; - let entPos = ent.position(); - let access = m.getLandAccess(gameState, ent); - for (let struct of gameState.getEnemyStructures().values()) - { - let structPos = struct.position(); - let x; - let z; - if (struct.hasClass("Field")) - { - if (!struct.resourceSupplyNumGatherers() || - !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) - continue; - } - let dist = API3.VectorDistance(entPos, structPos); - if (dist > range) - { - let safety = struct.footprintRadius() + 30; - x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; - z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; - let 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 (let [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; - } - } -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.AttackManager.prototype.update = function(gameState, queues, events) -{ - if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) - { - this.debugTime = gameState.ai.elapsedTime; - API3.warn(" upcoming attacks ================="); - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" started attacks =================="); - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" =================================="); - } - - this.checkEvents(gameState, events); - - let unexecutedAttacks = { "Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0 }; - for (let attackType in this.upcomingAttacks) - { - for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) - { - let attack = this.upcomingAttacks[attackType][i]; - attack.checkEvents(gameState, events); - - if (attack.isStarted()) - API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); - - let updateStep = attack.updatePreparation(gameState); - // now we're gonna check if the preparation time is over - if (updateStep == 1 || attack.isPaused()) - { - // just chillin' - if (attack.state == "unexecuted") - ++unexecutedAttacks[attackType]; - } - else if (updateStep == 0) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - else if (updateStep == 2) - { - if (attack.StartAttack(gameState)) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); - if (this.Config.chat) - m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); - this.startedAttacks[attackType].push(attack); - } - else - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - } - } - - for (let attackType in this.startedAttacks) - { - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - attack.checkEvents(gameState, events); - // okay so then we'll update the attack. - if (attack.isPaused()) - continue; - let remaining = attack.update(gameState, events); - if (!remaining) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } - } - - // creating plans after updating because an aborted plan might be reused in that case. - - let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; - if (this.rushNumber < this.maxRushes && barracksNb >= 1) - { - if (unexecutedAttacks.Rush === 0) - { - // we have a barracks and we want to rush, rush. - let data = { "targetSize": this.rushSize[this.rushNumber] }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); - if (!attackPlan.failed) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Rush.push(attackPlan); - } - this.rushNumber++; - } - } - else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 && - this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) && - (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) - { - if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack - { - let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type); - if (attackPlan.failed) - this.attackPlansEncounteredWater = true; // hack - else - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[type].push(attackPlan); - } - this.attackNumber++; - } - } - - if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) - { - let target; - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - target = gameState.getEntityById(targetId); - if (!target) - 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 > 1 && gameState.ai.playedTurn % 5 == 0) - this.assignBombers(gameState); -}; - -m.AttackManager.prototype.getPlan = function(planName) -{ - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - for (let attackType in this.startedAttacks) - { - for (let attack of this.startedAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - return undefined; -}; - -m.AttackManager.prototype.pausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(false); -}; - -m.AttackManager.prototype.pauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(true); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(false); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(false); -}; - -m.AttackManager.prototype.getAttackInPreparation = function(type) -{ - 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. - */ -m.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; - - let veto = {}; - for (let i in this.defeated) - veto[i] = true; - // No rush if enemy too well defended (i.e. iberians) - if (attack.type == "Rush") - { - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (!gameState.isPlayerEnemy(i) || veto[i]) - continue; - if (this.defeated[i]) - continue; - let enemyDefense = 0; - for (let ent of gameState.getEnemyStructures(i).values()) - if (ent.hasClass("Tower") || ent.hasClass("Fortress")) - enemyDefense++; - if (enemyDefense > 6) - veto[i] = true; - } - } - - // then if not a huge attack, continue attacking our previous target as long as it has some entities, - // otherwise target the most accessible one - if (attack.type != "HugeAttack") - { - if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && - !this.defeated[this.currentEnemyPlayer] && - gameState.isPlayerEnemy(this.currentEnemyPlayer) && - gameState.getEntities(this.currentEnemyPlayer).hasEntities()) - return this.currentEnemyPlayer; - - let distmin; - let ccmin; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let ourcc of ccEnts.values()) - { - if (ourcc.owner() != PlayerID) - continue; - let ourPos = ourcc.position(); - let access = m.getLandAccess(gameState, ourcc); - for (let enemycc of ccEnts.values()) - { - if (veto[enemycc.owner()]) - continue; - if (!gameState.isPlayerEnemy(enemycc.owner())) - continue; - if (access != m.getLandAccess(gameState, enemycc)) - continue; - let dist = API3.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 (veto[i]) - continue; - if (!gameState.isPlayerEnemy(i)) - continue; - let enemyCount = 0; - let enemyCivCentre = false; - for (let 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; -}; - -/** - * Target the player with the most advanced wonder. - * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. - */ -m.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let enemyWonder; - let moreAdvanced; - for (let wonder of gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")).values()) - { - if (wonder.owner() == 0) - continue; - let progress = wonder.foundationProgress(); - if (progress === undefined) - { - enemyWonder = wonder; - break; - } - if (enemyWonder && moreAdvanced > progress) - continue; - enemyWonder = wonder; - moreAdvanced = progress; - } - if (enemyWonder) - { - enemyPlayer = enemyWonder.owner(); - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - } - return enemyPlayer; -}; - -/** - * Target the player with the most relics (including gaia). - */ -m.AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let allRelics = gameState.updatingGlobalCollection("allRelics", API3.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; - - let 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. */ -m.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - if (attack.targetPlayer === player) - attack.targetPlayer = undefined; - - for (let attackType in this.startedAttacks) - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - if (attack.targetPlayer === player) - { - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } -}; - -m.AttackManager.prototype.raidTargetEntity = function(gameState, ent) -{ - let data = { "target": ent }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); - if (attackPlan.failed) - return null; - if (this.Config.debug > 1) - API3.warn("Military Manager: Raiding plan " + this.totalNumber); - this.raidNumber++; - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Raid.push(attackPlan); - return attackPlan; -}; - -/** - * Return the number of units from any of our attacking armies around this position - */ -m.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) -{ - let num = 0; - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - { - if (!attack.position) // this attack may be inside a transport - continue; - if (API3.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 - */ -m.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) -{ - if (!target || !target.position()) - return false; - if (!data.range && !data.armyID) - { - API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); - return false; - } - let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; - let pos = target.position(); - let attackType = "Attack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); - if (attackPlan.failed) - return false; - this.totalNumber++; - attackPlan.init(gameState); - this.startedAttacks[attackType].push(attackPlan); - - let targetAccess = m.getLandAccess(gameState, target); - for (let army of gameState.ai.HQ.defenseManager.armies) - { - if (data.range) - { - army.recalculatePosition(gameState); - if (API3.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) - { - let unitId = army.ownEntities[0]; - army.removeOwn(gameState, unitId); - let unit = gameState.getEntityById(unitId); - let accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || - unit.position() && m.getLandAccess(gameState, unit) == targetAccess; - if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) - { - unit.setMetadata(PlayerID, "plan", attackPlan.name); - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.unitCollection.updateEnt(unit); - } - } - } - if (!attackPlan.unitCollection.hasEntities()) - { - attackPlan.Abort(gameState); - return false; - } - for (let unit of attackPlan.unitCollection.values()) - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.targetPlayer = target.owner(); - attackPlan.targetPos = pos; - attackPlan.target = target; - attackPlan.state = "arrived"; - return true; -}; - -m.AttackManager.prototype.Serialize = function() -{ - let properties = { - "totalNumber": this.totalNumber, - "attackNumber": this.attackNumber, - "rushNumber": this.rushNumber, - "raidNumber": this.raidNumber, - "debugTime": this.debugTime, - "maxRushes": this.maxRushes, - "rushSize": this.rushSize, - "currentEnemyPlayer": this.currentEnemyPlayer, - "defeated": this.defeated - }; - - let upcomingAttacks = {}; - for (let key in this.upcomingAttacks) - { - upcomingAttacks[key] = []; - for (let attack of this.upcomingAttacks[key]) - upcomingAttacks[key].push(attack.Serialize()); - } - - let startedAttacks = {}; - for (let key in this.startedAttacks) - { - startedAttacks[key] = []; - for (let attack of this.startedAttacks[key]) - startedAttacks[key].push(attack.Serialize()); - } - - return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; -}; - -m.AttackManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.upcomingAttacks = {}; - for (let key in data.upcomingAttacks) - { - this.upcomingAttacks[key] = []; - for (let dataAttack of data.upcomingAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.upcomingAttacks[key].push(attack); - } - } - - this.startedAttacks = {}; - for (let key in data.startedAttacks) - { - this.startedAttacks[key] = []; - for (let dataAttack of data.startedAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.startedAttacks[key].push(attack); - } - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/baseManager.js b/install/petraBased/petra-patriot/baseManager.js deleted file mode 100644 index b2260f6..0000000 --- a/install/petraBased/petra-patriot/baseManager.js +++ /dev/null @@ -1,1111 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Base Manager - * Handles lower level economic stuffs. - * Some tasks: - * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. - * -giving feedback/estimates on GR - * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. - * -getting good spots for dropsites - * -managing dropsite use in the base - * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) - */ - -m.BaseManager = function(gameState, Config) -{ - this.Config = Config; - this.ID = gameState.ai.uniqueIDs.bases++; - - // anchor building: seen as the main building of the base. Needs to have territorial influence - this.anchor = undefined; - this.anchorId = undefined; - this.accessIndex = undefined; - - // Maximum distance (from any dropsite) to look for resources - // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max - this.maxDistResourceSquare = 360*360; - - this.constructing = false; - // Defenders to train in this cc when its construction is finished - this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; - - // vector for iterating, to check one use the HQ map. - this.territoryIndices = []; - - this.timeNextIdleCheck = 0; -}; - -m.BaseManager.prototype.init = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else if (state != "captured") - this.neededDefenders = 0; - this.workerObject = new m.Worker(this); - // entitycollections - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); - this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.mobileDropsites = this.units.filter(API3.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 (let res of Resources.GetCodes()) - { - this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; - this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; - } -}; - -m.BaseManager.prototype.reset = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else - this.constructing = false; - - if (state != "captured" || this.Config.difficulty < 3) - this.neededDefenders = 0; - else - this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); -}; - -m.BaseManager.prototype.assignEntity = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "base", this.ID); - this.units.updateEnt(ent); - this.workers.updateEnt(ent); - this.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) - this.assignResourceToDropsite(gameState, ent); -}; - -m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) -{ - if (!anchorEntity.hasClass("CivCentre")) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); - else - { - this.anchor = anchorEntity; - this.anchorId = anchorEntity.id(); - this.anchor.setMetadata(PlayerID, "baseAnchor", true); - gameState.ai.HQ.resetBaseCache(); - } - anchorEntity.setMetadata(PlayerID, "base", this.ID); - this.buildings.updateEnt(anchorEntity); - this.accessIndex = m.getLandAccess(gameState, anchorEntity); - return true; -}; - -/* we lost our anchor. Let's reaffect our units and buildings */ -m.BaseManager.prototype.anchorLost = function(gameState, ent) -{ - this.anchor = undefined; - this.anchorId = undefined; - this.neededDefenders = 0; - gameState.ai.HQ.resetBaseCache(); -}; - -/** Set a building of an anchorless base */ -m.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) -{ - if (!this.buildings.hasEntities()) - { - if (!m.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); - this.accessIndex = m.getLandAccess(gameState, ent); - } - else if (this.accessIndex != m.getLandAccess(gameState, ent)) - API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + - " has been assigned " + ent.templateName() + " with access" + m.getLandAccess(gameState, ent)); - - 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. - */ -m.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) -{ - if (this.dropsites[dropsite.id()]) - { - if (this.Config.debug > 0) - warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); - return; - } - - let accessIndex = this.accessIndex; - let dropsitePos = dropsite.position(); - let dropsiteId = dropsite.id(); - this.dropsites[dropsiteId] = true; - - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) - accessIndex = m.getLandAccess(gameState, dropsite); - - let maxDistResourceSquare = this.maxDistResourceSquare; - for (let type of dropsite.resourceDropsiteTypes()) - { - let resources = gameState.getResourceSupplies(type); - if (!resources.length) - continue; - - let nearby = this.dropsiteSupplies[type].nearby; - let medium = this.dropsiteSupplies[type].medium; - let faraway = this.dropsiteSupplies[type].faraway; - - resources.forEach(function(supply) - { - if (!supply.position()) - return; - if (supply.hasClass("Animal")) // moving resources are treated differently - return; - if (supply.hasClass("Field")) // fields are treated separately - return; - if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately - return; - // quick accessibility check - if (m.getLandAccess(gameState, supply) != accessIndex) - return; - - let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); - if (dist < maxDistResourceSquare) - { - if (dist < maxDistResourceSquare/16) // distmax/4 - nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else if (dist < maxDistResourceSquare/4) // distmax/2 - medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else - faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - } - }); - - nearby.sort((r1, r2) => r1.dist - r2.dist); - medium.sort((r1, r2) => r1.dist - r2.dist); - faraway.sort((r1, r2) => r1.dist - r2.dist); - -/* let debug = false; - if (debug) - { - faraway.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); - }); - medium.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); - }); - nearby.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); - }); - } */ - } - - // Allows all allies to use this dropsite except if base anchor to be sure to keep - // a minimum of resources for this base - Engine.PostCommand(PlayerID, { - "type": "set-dropsite-sharing", - "entities": [dropsiteId], - "shared": dropsiteId != this.anchorId - }); -}; - -// completely remove the dropsite resources from our list. -m.BaseManager.prototype.removeDropsite = function(gameState, ent) -{ - if (!ent.id()) - return; - - let removeSupply = function(entId, supply){ - for (let i = 0; i < supply.length; ++i) - { - // exhausted resource, remove it from this list - if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) - supply.splice(i--, 1); - // resource assigned to the removed dropsite, remove it - else if (supply[i].dropsite == entId) - supply.splice(i--, 1); - } - }; - - for (let type in this.dropsiteSupplies) - { - removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); - removeSupply(ent.id(), this.dropsiteSupplies[type].medium); - removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); - } - - this.dropsites[ent.id()] = undefined; -}; - -/** - * Returns the position of the best place to build a new dropsite for the specified resource - */ -m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) -{ - - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then checks for a good spot in the territory. If none, and town/city phase, checks outside - // The AI will currently not build a CC if it wouldn't connect with an existing CC. - - let obstructions = m.createObstructionMap(gameState, this.accessIndex, template); - - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray(); - - let bestIdx; - let bestVal = 0; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let cellSize = territoryMap.cellSize; - - for (let j of this.territoryIndices) - { - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) // no room around - continue; - - // we add 3 times the needed resource and once the others (except food) - let total = 2*gameState.sharedScript.resourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - total += gameState.sharedScript.resourceMaps[res].map[j]; - - total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 - if (total <= bestVal) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - for (let dp of dpEnts) - { - let dpPos = dp.position(); - if (!dpPos) - continue; - let dist = API3.SquareVectorDistance(dpPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - - for (let cc of ccEnts) - { - let ccPos = cc.position(); - if (!ccPos) - continue; - let dist = API3.SquareVectorDistance(ccPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = total; - bestIdx = i; - } - - if (this.Config.debug > 2) - warn(" for dropsite best is " + bestVal); - - if (bestVal <= 0) - return { "quality": bestVal, "pos": [0, 0] }; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return { "quality": bestVal, "pos": [x, z] }; -}; - -m.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) -{ - let count = 0; - let check = {}; - for (let supply of this.dropsiteSupplies[type].nearby) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - count += supply.ent.resourceSupplyAmount(); - } - if (nearbyOnly) - return count; - - for (let supply of this.dropsiteSupplies[type].medium) - { - if (check[supply.id]) - continue; - check[supply.id] = true; - count += 0.6*supply.ent.resourceSupplyAmount(); - } - return count; -}; - -/** check our resource levels and react accordingly */ -m.BaseManager.prototype.checkResourceLevels = function(gameState, queues) -{ - for (let type of Resources.GetCodes()) - { - if (type == "food") - { - if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations - let numQueue = queues.field.countQueuedUnits(); - - // TODO if not yet farms, add a check on time used/lost and build farmstead if needed - if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them - { - if (count < 600) - { - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID })); - gameState.ai.HQ.needFarm = true; - } - } - else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) - { - let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; - let goal = this.Config.Economy.provisionFields; - if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) - goal = Math.max(goal-1, 1); - if (numFound + numQueue < goal) - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "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 m.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")) - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - if (count < 900) - { - queues.corral.addPlan(new m.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(API3.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 (let ent of this.gatherersByType(gameState, type).values()) - { - if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") - ++this.gatherers[type].used; - else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - ++this.gatherers[type].lost; - } - // TODO add also a test on remaining resources. - let total = this.gatherers[type].used + this.gatherers[type].lost; - if (total > 150 || total > 60 && type != "wood") - { - let ratio = this.gatherers[type].lost / total; - if (ratio > 0.15) - { - let newDP = this.findBestDropsiteLocation(gameState, type); - if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) - { - // No good dropsite, try to build a new base if no base already planned, - // and if not possible, be less strict on dropsite quality. - if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && - newDP.quality > Math.min(25, 50*0.15/ratio) && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - } - } - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; - this.gatherers[type].used = 0; - this.gatherers[type].lost = 0; - } - else if (total == 0) - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; - } - -}; - -/** Adds the estimated gather rates from this base to the currentRates */ -m.BaseManager.prototype.addGatherRates = function(gameState, currentRates) -{ - for (let res in currentRates) - { - // I calculate the exact gathering rate for each unit. - // I must then lower that to account for travel time. - // Given that the faster you gather, the more travel time matters, - // I use some logarithms. - // TODO: this should take into account for unit speed and/or distance to target - - this.gatherersByType(gameState, res).forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - if (res == "food") - { - this.workersBySubrole(gameState, "hunter").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - this.workersBySubrole(gameState, "fisher").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - } - } -}; - -m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) -{ - if (!roleless) - roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); - - for (let ent of roleless) - { - if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "role", "worker"); - else if (ent.hasClass("Support") && ent.hasClass("Elephant")) - ent.setMetadata(PlayerID, "role", "worker"); - } -}; - -/** - * If the numbers of workers on the resources is unbalanced then set some of workers to idle so - * they can be reassigned by reassignIdleWorkers. - * TODO: actually this probably should be in the HQ. - */ -m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) -{ - this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; - // change resource only towards one which is more needed, and if changing will not change this order - let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let sumWanted = 0; - let sumCurrent = 0; - for (let need of mostNeeded) - { - sumWanted += need.wanted; - sumCurrent += need.current; - } - let scale = 1; - if (sumWanted > 0) - scale = sumCurrent / sumWanted; - - for (let i = mostNeeded.length-1; i > 0; --i) - { - let lessNeed = mostNeeded[i]; - for (let j = 0; j < i; ++j) - { - let moreNeed = mostNeeded[j]; - let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) - { - if (lessNeed.type != "food" && gameState.ai.HQ.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" && gameState.ai.HQ.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 FemaleCitizen for food and CitizenSoldier for other resources. - */ -m.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) -{ - let num = number; - let only; - let gatherers = this.gatherersByType(gameState, from); - if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) - only = "CitizenSoldier"; - else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) - only = "FemaleCitizen"; - - for (let 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); - gameState.ai.HQ.AddTCResGatherer(to); - } - return num; -}; - -m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) -{ - // Search for idle workers, and tell them to gather resources based on demand - if (!idleWorkers) - { - let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); - idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); - } - - for (let ent of idleWorkers) - { - // Check that the worker isn't garrisoned - if (!ent.position()) - continue; - // Support elephant can only be builders - if (ent.hasClass("Support") && ent.hasClass("Elephant")) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - continue; - } - - if (ent.hasClass("Worker")) - { - // Just emergency repairing here. It is better managed in assignToFoundations - if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && - gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) - ent.repair(this.anchor); - else if (ent.isGatherer()) - { - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - for (let needed of mostNeeded) - { - if (!ent.canGather(needed.type)) - continue; - let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) - continue; - ent.setMetadata(PlayerID, "subrole", "gatherer"); - ent.setMetadata(PlayerID, "gather-type", needed.type); - gameState.ai.HQ.AddTCResGatherer(needed.type); - break; - } - } - } - else if (ent.hasClass("Cavalry")) - ent.setMetadata(PlayerID, "subrole", "hunter"); - else if (ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "subrole", "fisher"); - } -}; - -m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) -{ - return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); -}; - -m.BaseManager.prototype.gatherersByType = function(gameState, type) -{ - return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); -}; - -/** - * returns an entity collection of workers. - * They are idled immediatly and their subrole set to idle. - */ -m.BaseManager.prototype.pickBuilders = function(gameState, workers, number) -{ - let availableWorkers = this.workers.filter(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") == "builder") - vala = 100; - if (b.getMetadata(PlayerID, "subrole") == "builder") - valb = 100; - if (a.getMetadata(PlayerID, "subrole") == "idle") - vala = -50; - if (b.getMetadata(PlayerID, "subrole") == "idle") - valb = -50; - if (a.getMetadata(PlayerID, "plan") === undefined) - vala = -20; - if (b.getMetadata(PlayerID, "plan") === undefined) - valb = -20; - return vala - valb; - }); - let needed = Math.min(number, availableWorkers.length - 3); - for (let i = 0; i < needed; ++i) - { - availableWorkers[i].stopMoving(); - availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); - workers.addEnt(availableWorkers[i]); - } - return; -}; - -/** - * If we have some foundations, and we don't have enough builder-workers, - * try reassigning some other workers who are nearby - * AI tries to use builders sensibly, not completely stopping its econ. - */ -m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) -{ - let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); - - let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); - - // Check if nothing to build - if (!foundations.length && !damagedBuildings.length) - return; - - let workers = this.workers.filter(ent => ent.isBuilder()); - let builderWorkers = this.workersBySubrole(gameState, "builder"); - let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); - - // if we're constructing and we have the foundations to our base anchor, only try building that. - if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) - { - foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); - let tID = foundations.toEntityArray()[0].id(); - workers.forEach(ent => { - let target = ent.getMetadata(PlayerID, "target-foundation"); - if (target && target != tID) - { - ent.stopMoving(); - ent.setMetadata(PlayerID, "target-foundation", tID); - } - }); - } - - if (workers.length < 3) - { - let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); - if (fromOtherBase) - { - let baseID = this.ID; - fromOtherBase.forEach(worker => { - worker.setMetadata(PlayerID, "base", baseID); - worker.setMetadata(PlayerID, "subrole", "builder"); - workers.updateEnt(worker); - builderWorkers.updateEnt(worker); - idleBuilderWorkers.updateEnt(worker); - }); - } - } - - let builderTot = builderWorkers.length - idleBuilderWorkers.length; - - // Make the limit on number of builders depends on the available resources - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - let builderRatio = 1; - for (let 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 (let target of foundations.values()) - { - if (target.hasClass("Field")) - continue; // we do not build fields - - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) - continue; - - // if our territory has shrinked since this foundation was positioned, do not build it - if (m.isNotWorthBuilding(gameState, target)) - continue; - - let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - let maxTotalBuilders = Math.ceil(workers.length * 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.hasClass("Fortress") || target.hasClass("Wonder") || - target.getMetadata(PlayerID, "phaseUp") == true) - targetNB = 7; - else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || - target.hasClass("Market")) - targetNB = 4; - else if (target.hasClass("House") || target.hasClass("DropsiteWood")) - targetNB = 3; - - if (target.getMetadata(PlayerID, "baseAnchor") == true || - target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) - { - targetNB = 15; - maxTotalBuilders = Math.max(maxTotalBuilders, 15); - } - - // if no base yet, everybody should build - if (gameState.ai.HQ.numActiveBases() == 0) - { - targetNB = workers.length; - maxTotalBuilders = targetNB; - } - - if (assigned >= targetNB) - continue; - idleBuilderWorkers.forEach(function(ent) { - if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (assigned >= targetNB || !ent.position() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }).toEntityArray(); - let time = target.buildTime(); - nonBuilderWorkers.sort((workerA, workerB) => { - let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); - // elephant moves slowly, so when far away they are only useful if build time is long - if (workerA.hasClass("Elephant")) - coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); - else if (workerA.getMetadata(PlayerID, "gather-type") == "food") - coeffA *= 3; - let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); - if (workerB.hasClass("Elephant")) - coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); - else if (workerB.getMetadata(PlayerID, "gather-type") == "food") - coeffB *= 3; - return coeffA - coeffB; - }); - let current = 0; - let nonBuilderTot = nonBuilderWorkers.length; - while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) - { - ++assigned; - ++builderTot; - let ent = nonBuilderWorkers[current++]; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - } - } - - for (let target of damagedBuildings.values()) - { - // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building - // that's being destroyed. - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - { - if (target.healthLevel() > 0.5 || - !target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!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.hasClass("Fortress") || target.hasClass("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() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }); - let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); - let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); - - nearestNonBuilders.forEach(function(ent) { - ++assigned; - ++builderTot; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - } -}; - -/** Return false when the base is not active (no workers on it) */ -m.BaseManager.prototype.update = function(gameState, queues, events) -{ - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units - { - // if some active base, reassigns the workers/buildings - // otherwise look for anything useful to do, i.e. treasures to gather - if (gameState.ai.HQ.numActiveBases() > 0) - { - for (let ent of this.units.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (bestBase.ID != this.ID) - bestBase.assignEntity(gameState, ent); - } - for (let ent of this.buildings.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (!bestBase) - { - if (ent.hasClass("Dock")) - API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); - continue; - } - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - 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 (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let 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 (let ent of this.units.values()) - { - let base = m.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 (let ent of this.buildings.values()) - { - if (!ent.position()) - continue; - let base = m.getBestBase(gameState, ent); - if (base.anchor) - reassignedBase = base; - break; - } - - if (reassignedBase) - { - for (let ent of this.units.values()) - reassignedBase.assignEntity(gameState, ent); - for (let ent of this.buildings.values()) - { - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - reassignedBase.assignEntity(gameState, ent); - } - return false; - } - - this.assignToFoundations(gameState); - if (gameState.ai.elapsedTime > this.timeNextIdleCheck) - this.setWorkersIdleByPriority(gameState); - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - return true; - } - - Engine.ProfileStart("Base update - base " + this.ID); - - this.checkResourceLevels(gameState, queues); - this.assignToFoundations(gameState); - - if (this.constructing) - { - let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); - if(owner != 0 && !gameState.isPlayerAlly(owner)) - { - // we're in enemy territory. If we're too close from the enemy, destroy us. - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (cc.owner() != owner) - continue; - if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) - continue; - this.anchor.destroy(); - gameState.ai.HQ.resetBaseCache(); - break; - } - } - } - else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) - --this.neededDefenders; - - if (gameState.ai.elapsedTime > this.timeNextIdleCheck && - (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) - this.setWorkersIdleByPriority(gameState); - - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - // check if workers can find something useful to do - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - - Engine.ProfileStop(); - return true; -}; - -m.BaseManager.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "anchorId": this.anchorId, - "accessIndex": this.accessIndex, - "maxDistResourceSquare": this.maxDistResourceSquare, - "constructing": this.constructing, - "gatherers": this.gatherers, - "neededDefenders": this.neededDefenders, - "territoryIndices": this.territoryIndices, - "timeNextIdleCheck": this.timeNextIdleCheck - }; -}; - -m.BaseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-patriot/buildManager.js b/install/petraBased/petra-patriot/buildManager.js deleted file mode 100644 index fa78ca2..0000000 --- a/install/petraBased/petra-patriot/buildManager.js +++ /dev/null @@ -1,173 +0,0 @@ -var PETRA = function(m) -{ - -/** - * 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. - */ - -m.BuildManager = function() -{ - // List of buildings we have builders for, with number of possible builders. - this.builderCounters = 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(); -}; - -/** Initialization at start of game */ -m.BuildManager.prototype.init = function(gameState) -{ - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnUnits().values()) - this.incrementBuilderCounters(civ, ent, 1); -}; - -m.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) -{ - for (let buildable of ent.buildableEntities(civ)) - { - if (this.builderCounters.has(buildable)) - { - let count = this.builderCounters.get(buildable) + increment; - if (count < 0) - { - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); - continue; - } - this.builderCounters.set(buildable, count); - } - else if (increment > 0) - this.builderCounters.set(buildable, increment); - else - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); - } -}; - -/** Update the builders counters */ -m.BuildManager.prototype.checkEvents = function(gameState, events) -{ - this.elapsedTime = gameState.ai.elapsedTime; - let civ = gameState.getPlayerCiv(); - - for (let evt of events.Create) - { - if (events.Destroy.some(e => e.entity == evt.entity)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, 1); - } - - for (let evt of events.Destroy) - { - if (events.Create.some(e => e.entity == evt.entity) || !evt.entityObj) - continue; - let ent = evt.entityObj; - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, -1); - } - - for (let evt of events.OwnershipChanged) // capture events - { - let increment; - if (evt.from == PlayerID) - increment = -1; - else if (evt.to == PlayerID) - increment = 1; - else - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, increment); - } -}; - - -/** - * Get the first buildable structure with a given class - * TODO when several available, choose the best one - */ -m.BuildManager.prototype.findStructureWithClass = function(gameState, classes) -{ - for (let [templateName, count] of this.builderCounters) - { - if (count == 0 || gameState.isTemplateDisabled(templateName)) - continue; - let template = gameState.getTemplate(templateName); - if (!template || !template.available(gameState)) - continue; - if (MatchesClassList(template.classes(), classes)) - return templateName; - } - return undefined; -}; - -m.BuildManager.prototype.hasBuilder = function(template) -{ - let numBuilders = this.builderCounters.get(template); - return numBuilders && numBuilders > 0; -}; - -m.BuildManager.prototype.isUnbuildable = function(gameState, template) -{ - return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; -}; - -m.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 */ -m.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 - { - let 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 */ -m.BuildManager.prototype.numberMissingRoom = function(gameState) -{ - let num = 0; - for (let unbuildable of this.unbuildables.values()) - if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) - ++num; - return num; -}; - -/** Reset the unbuildables due to missing room */ -m.BuildManager.prototype.resetMissingRoom = function(gameState) -{ - for (let [key, unbuildable] of this.unbuildables) - if (unbuildable.reason == "room") - this.unbuildables.delete(key); -}; - -m.BuildManager.prototype.Serialize = function() -{ - return { - "builderCounters": this.builderCounters, - "unbuildables": this.unbuildables - }; -}; - -m.BuildManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/chatHelper.js b/install/petraBased/petra-patriot/chatHelper.js deleted file mode 100644 index 8afbcfc..0000000 --- a/install/petraBased/petra-patriot/chatHelper.js +++ /dev/null @@ -1,242 +0,0 @@ -var PETRA = function(m) -{ - -m.launchAttackMessages = { - "hugeAttack": [ - markForTranslation("I am starting a massive military campaign against %(_player_)s, come and join me."), - markForTranslation("I have set up a huge army to crush %(_player_)s. Join me and you will have your share of the loot.") - ], - "other": [ - markForTranslation("I am launching an attack against %(_player_)s."), - markForTranslation("I have just sent an army against %(_player_)s.") - ] -}; - -m.answerRequestAttackMessages = { - "join": [ - markForTranslation("Let me regroup my army and I will then join you against %(_player_)s."), - markForTranslation("I am finishing preparations to attack %(_player_)s.") - ], - "decline": [ - markForTranslation("Sorry, I do not have enough soldiers currently; but my next attack will target %(_player_)s."), - markForTranslation("Sorry, I still need to strengthen my army. However, I will attack %(_player_)s next.") - ], - "other": [ - markForTranslation("I cannot help you against %(_player_)s for the time being, I am planning to attack %(_player_2)s first.") - ] -}; - -m.sentTributeMessages = [ - markForTranslation("Here is a gift for you, %(_player_)s. Make good use of it."), - markForTranslation("I see you are in a bad situation, %(_player_)s. I hope this helps."), - markForTranslation("I can help you this time, %(_player_)s, but you should manage your resources more carefully in the future.") -]; - -m.requestTributeMessages = [ - markForTranslation("I am in need of %(resource)s, can you help? I will make it up to you."), - markForTranslation("I would participate more efficiently in our common war effort if you could provide me some %(resource)s."), - markForTranslation("If you can spare me some %(resource)s, I will be able to strengthen my army.") -]; - -m.newTradeRouteMessages = [ - markForTranslation("I have set up a new route with %(_player_)s. Trading will be profitable for all of us."), - markForTranslation("A new trade route is set up with %(_player_)s. Take your share of the profits.") -]; - -m.newDiplomacyMessages = { - "ally": [ - markForTranslation("%(_player_)s and I are now allies.") - ], - "neutral": [ - markForTranslation("%(_player_)s and I are now neutral.") - ], - "enemy": [ - markForTranslation("%(_player_)s and I are now enemies.") - ] -}; - -m.answerDiplomacyRequestMessages = { - "ally": { - "decline": [ - markForTranslation("I cannot accept your offer to become allies, %(_player_)s.") - ], - "declineSuggestNeutral": [ - markForTranslation("I will not be your ally, %(_player_)s. However, I will consider a neutrality pact."), - markForTranslation("I reject your request for alliance, %(_player_)s, but we could become neutral."), - markForTranslation("%(_player_)s, only a neutrality agreement is conceivable to me.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous alliance did not work out, %(_player_)s. I must decline your offer."), - markForTranslation("I won’t ally you again, %(_player_)s!"), - markForTranslation("No more alliances between us, %(_player_)s!"), - markForTranslation("Your request for peace means nothing to me anymore, %(_player_)s!"), - markForTranslation("My answer to your repeated peace proposal will remain war, %(_player_)s!") - ], - "accept": [ - markForTranslation("I will accept your offer to become allies, %(_player_)s. We will both benefit from this partnership."), - markForTranslation("An alliance between us is a good idea, %(_player_)s."), - markForTranslation("Let both of our people prosper from a peaceful association, %(_player_)s."), - markForTranslation("We have found common ground, %(_player_)s. I accept the alliance."), - markForTranslation("%(_player_)s, consider us allies from now on.") - ], - "acceptWithTribute": [ - markForTranslation("I will ally with you, %(_player_)s, but only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("%(_player_)s, you must send me a tribute of %(_amount_)s %(_resource_)s before I accept an alliance with you."), - markForTranslation("Unless you send me %(_amount_)s %(_resource_)s, an alliance won’t be formed, %(_player_)s,") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, my offer still stands. I will ally with you only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("I’m still waiting for %(_amount_)s %(_resource_)s before accepting your alliance, %(_player_)s."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - }, - "neutral": { - "decline": [ - markForTranslation("I will not become neutral with you, %(_player_)s."), - markForTranslation("%(_player_)s, I must decline your request for a neutrality pact.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous neutrality agreement ended in failure, %(_player_)s; I will not consider another one.") - ], - "accept": [ - markForTranslation("I welcome your request for peace between our civilizations, %(_player_)s. I will accept."), - markForTranslation("%(_player_)s, I will accept your neutrality request. May both our civilizations benefit.") - ], - "acceptWithTribute": [ - markForTranslation("If you send me a tribute of %(_amount_)s %(_resource_)s, I will accept your neutrality request, %(_player_)s."), - markForTranslation("%(_player_)s, if you send me %(_amount_)s %(_resource_)s, I will accept a neutrality pact.") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, I will not accept your neutrality request unless you tribute me %(_amount_)s %(_resource_)s soon."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - } -}; - -m.sendDiplomacyRequestMessages = { - "ally": { - "sendRequest": [ - markForTranslation("%(_player_)s, it would help both of our civilizations if we formed an alliance. If you become allies with me, I will respond in kind.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, my offer for an alliance has expired."), - markForTranslation("%(_player_)s, I have rescinded my previous offer for an alliance between us."), - ] - }, - "neutral": { - "sendRequest": [ - markForTranslation("%(_player_)s, I would like to request a neutrality pact between our civilizations. If you become neutral with me, I will respond in kind."), - markForTranslation("%(_player_)s, it would be both to our benefit if we negotiated a neutrality pact. I will become neutral with you if you do the same.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, I have decided to revoke my offer for a neutrality pact."), - markForTranslation("%(_player_)s, as you have failed to respond to my request for peace between us, I have abrogated my offer."), - ] - } -}; - -m.chatLaunchAttack = function(gameState, player, type) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.launchAttackMessages[type === "HugeAttack" ? "hugeAttack" : "other"]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestAttack = function(gameState, player, answer, other) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.answerRequestAttackMessages[answer]), - "translateMessage": true, - "translateParameters": answer != "other" ? ["_player_"] : ["_player_", "_player_2"], - "parameters": answer != "other" ? { "_player_": player } : { "_player_": player, "_player_2": other } - }); -}; - -m.chatSentTribute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.sentTributeMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatRequestTribute = function(gameState, resource) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.requestTributeMessages), - "translateMessage": true, - "translateParameters": { "resource": "withinSentence" }, - "parameters": { "resource": Resources.GetNames()[resource] } - }); -}; - -m.chatNewTradeRoute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newTradeRouteMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatNewPhase = function(gameState, phase, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newPhaseMessages[status]), - "translateMessage": true, - "translateParameters": ["phase"], - "parameters": { "phase": phase } - }); -}; - -m.chatNewDiplomacy = function(gameState, player, newDiplomaticStance) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": pickRandom(this.newDiplomacyMessages[newDiplomaticStance]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestDiplomacy = function(gameState, player, requestType, response, requiredTribute) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.answerDiplomacyRequestMessages[requestType][response]), - "translateMessage": true, - "translateParameters": requiredTribute ? ["_amount_", "_resource_", "_player_"] : ["_player_"], - "parameters": requiredTribute ? - { "_amount_": requiredTribute.wanted, "_resource_": requiredTribute.type, "_player_": player } : - { "_player_": player } - }); -}; - -m.chatNewRequestDiplomacy = function(gameState, player, requestType, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.sendDiplomacyRequestMessages[requestType][status]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/config.js b/install/petraBased/petra-patriot/config.js deleted file mode 100644 index 77e4b56..0000000 --- a/install/petraBased/petra-patriot/config.js +++ /dev/null @@ -1,257 +0,0 @@ -var PETRA = function(m) -{ - -m.Config = function(difficulty, behavior) -{ - // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. - this.difficulty = difficulty !== undefined ? difficulty : 3; - - // 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; - - this.chat = true; // false to prevent AI's chats - - this.popScaling = 1; // scale factor depending on the max population - - this.Military = { - "towerLapseTime": 90, // Time to wait between building 2 towers - "fortressLapseTime": 390, // Time to wait between building 2 fortresses - "popForBarracks1": 25, - "popForBarracks2": 95, - "popForBlacksmith": 65, - "numSentryTowers": 1 - }; - this.Economy = { - "popPhase2": 38, // How many units we want before aging to phase2. - "workPhase3": 65, // How many workers we want before aging to phase3. - "workPhase4": 80, // How many workers we want before aging to phase4 or higher. - "popForDock": 25, - "targetNumWorkers": 40, // dummy, will be changed later - "targetNumTraders": 5, // Target number of traders - "targetNumFishers": 1, // Target number of fishers per sea - "supportRatio": 0.35, // fraction of support workers among the workforce - "provisionFields": 2 - }; - - // Note: attack settings are set directly in attack_plan.js - // defense - this.Defense = - { - "defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. - "armyCompactSize": 2000, // squared. Half-diameter of an army. - "armyBreakawaySize": 3500, // squared. - "armyMergeSize": 1400 // squared. - }; - - // 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 = - { - "default": [], - "athen": ["structures/{civ}_gymnasion", "structures/{civ}_prytaneion", - "structures/{civ}_theatron", "structures/{civ}_royal_stoa"], - "brit": ["structures/{civ}_rotarymill"], - "cart": ["structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", - "structures/{civ}_embassy_italiote"], - "gaul": ["structures/{civ}_rotarymill", "structures/{civ}_tavern"], - "iber": ["structures/{civ}_monument"], - "kush": ["structures/{civ}_pyramid_large", "structures/{civ}_blemmye_camp", - "structures/{civ}_nuba_village"], - "mace": ["structures/{civ}_library", "structures/{civ}_theatron"], - "maur": ["structures/{civ}_pillar_ashoka"], - "pers": ["structures/{civ}_apadana", "structures/{civ}_hall"], - "ptol": ["structures/{civ}_library"], - "rome": ["structures/{civ}_army_camp"], - "sele": ["structures/{civ}_library"], - "spart": ["structures/{civ}_syssiton", "structures/{civ}_theatron", - "structures/{civ}_royal_stoa"] - }; - - this.priorities = - { - "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food - "citizenSoldier": 60, - "trader": 50, - "healer": 20, - "ships": 70, - "house": 350, - "dropsites": 200, - "field": 400, - "dock": 90, - "corral": 100, - "economicBuilding": 90, - "militaryBuilding": 130, - "defenseBuilding": 70, - "civilCentre": 950, - "majorTech": 700, - "minorTech": 40, - "wonder": 1000, - "emergency": 1000 // used only in emergency situations, should be the highest one - }; - - // Default personality (will be updated in setConfig) - this.personality = - { - "aggressive": 0.5, - "cooperative": 0.5, - "defensive": 0.5 - }; - - // See m.QueueManager.prototype.wantedGatherRates() - this.queues = - { - "firstTurn": { - "food": 10, - "wood": 10, - "default": 0 - }, - "short": { - "food": 200, - "wood": 200, - "default": 100 - }, - "medium": { - "default": 0 - }, - "long": { - "default": 0 - } - }; - - this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; -}; - -m.Config.prototype.setConfig = function(gameState) -{ - if (this.difficulty > 0) - { - // 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 - let 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 } - }; - let behavior = randFloat(-0.5, 0.5); - // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety - let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); - let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; - let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; - let min = personalityList[this.behavior].min; - let 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 < 2) - { - this.Economy.supportRatio = 0.5; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else if (this.difficulty < 3) - { - this.Economy.supportRatio = 0.4; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else - { - if (this.difficulty == 3) - 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; - } - } - - let maxPop = gameState.getPopulationMax(); - if (this.difficulty < 2) - this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); - else if (this.difficulty < 3) - this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); - else - this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); - this.Economy.targetNumTraders = 2 + this.difficulty; - - - if (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.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * 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 < 2) - this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase - - if (this.debug < 2) - return; - API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); -}; - -m.Config.prototype.Serialize = function() -{ - var data = {}; - for (let key in this) - if (this.hasOwnProperty(key) && key != "debug") - data[key] = this[key]; - return data; -}; - -m.Config.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/defenseArmy.js b/install/petraBased/petra-patriot/defenseArmy.js deleted file mode 100644 index a5be2f2..0000000 --- a/install/petraBased/petra-patriot/defenseArmy.js +++ /dev/null @@ -1,651 +0,0 @@ -var PETRA = function(m) -{ - -/** Armies used by the defense manager. - * An army is a collection of own entities and enemy entities. - * - * Types of armies: - * "default": army to counter an invading army - * "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 - */ -m.DefenseArmy = function(gameState, foeEntities, type) -{ - 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 (let 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 - */ -m.DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) -{ - if (this.foeEntities.indexOf(enemyId) !== -1) - return false; - let ent = gameState.getEntityById(enemyId); - if (!ent || !ent.position()) - return false; - - // check distance - if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) - return false; - - this.foeEntities.push(enemyId); - this.assignedAgainst[enemyId] = []; - this.positionLastUpdate = 0; - this.evaluateStrength(ent); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - - return true; -}; - -/** - * returns true if the entity was removed and false otherwise. - * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. - */ -m.DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) -{ - let idx = this.foeEntities.indexOf(enemyId); - if (idx === -1) - return false; - - this.foeEntities.splice(idx, 1); - - this.assignedAgainst[enemyId] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] == enemyId) - this.assignedTo[to] = undefined; - - let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); - if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) - { - this.evaluateStrength(ent, false, true); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - } - - return true; -}; - -/** - * adds a defender but doesn't assign him yet. - * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship - */ -m.DefenseArmy.prototype.addOwn = function(gameState, id, force) -{ - if (this.ownEntities.indexOf(id) !== -1) - return false; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position() && !force) - return false; - - this.ownEntities.push(id); - this.evaluateStrength(ent, true); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - this.assignedTo[id] = 0; - - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole === undefined || subrole !== "defender") - ent.setMetadata(PlayerID, "formerSubrole", subrole); - ent.setMetadata(PlayerID, "subrole", "defender"); - return true; -}; - -m.DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) -{ - let idx = this.ownEntities.indexOf(id); - if (idx === -1) - return false; - - this.ownEntities.splice(idx, 1); - - if (this.assignedTo[id] !== 0) - { - let temp = this.assignedAgainst[this.assignedTo[id]]; - if (temp) - temp.splice(temp.indexOf(id), 1); - } - this.assignedTo[id] = undefined; - - let ent = Entity ? Entity : gameState.getEntityById(id); - if (!ent) - 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); - - let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); - if (formerSubrole !== undefined) - ent.setMetadata(PlayerID, "subrole", formerSubrole); - else - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "formerSubrole", undefined); - - // Remove from tranport plan if not yet on Board - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan && plan.state == "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. - */ -m.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 - let posOwn = [0, 0]; - let nOwn = 0; - let posAlly = [0, 0]; - let nAlly = 0; - let posOther = [0, 0]; - let nOther = 0; - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - let pos = ent.position(); - let 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; - let armyAccess = gameState.ai.accessibility.getAccessValue(posOther); - for (let struct of gameState.getAllyStructures().values()) - { - let pos = struct.position(); - if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) - continue; - if (m.getLandAccess(gameState, struct) !== armyAccess) - continue; - let defensiveStruct = struct.hasDefensiveFire(); - if (defensiveFound && !defensiveStruct) - continue; - let dist = API3.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) - { - let entId = this.ownEntities[0]; - this.removeOwn(gameState, entId); - let 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); -}; - -m.DefenseArmy.prototype.assignUnit = function(gameState, entID) -{ - // we'll assume this defender is ours already. - // we'll also override any previous assignment - - let ent = gameState.getEntityById(entID); - if (!ent || !ent.position()) - return false; - - // try to return its resources, and if any, the attack order will be queued - let queued = m.returnResources(gameState, ent); - - let idMin; - let distMin; - let idMinAll; - let distMinAll; - for (let id of this.foeEntities) - { - let eEnt = gameState.getEntityById(id); - if (!eEnt || !eEnt.position()) // probably can't happen. - 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") && !m.isSiegeUnit(eEnt)) - continue; - - let dist = API3.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; - - let ownIndex = m.getLandAccess(gameState, ent); - let foeEnt = gameState.getEntityById(idFoe); - let foePosition = foeEnt.position(); - let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); - if (ownIndex == foeIndex || ent.hasClass("Ship")) - { - this.assignedTo[entID] = idFoe; - this.assignedAgainst[idFoe].push(entID); - ent.attack(idFoe, m.allowCapture(gameState, ent, foeEnt), queued); - } - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); - return true; -}; - -m.DefenseArmy.prototype.getType = function() -{ - return this.type; -}; - -m.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). - */ -m.DefenseArmy.prototype.merge = function(gameState, otherArmy) -{ - // copy over all parameters. - for (let i in otherArmy.assignedAgainst) - { - if (this.assignedAgainst[i] === undefined) - this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; - else - this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); - } - for (let i in otherArmy.assignedTo) - this.assignedTo[i] = otherArmy.assignedTo[i]; - - for (let id of otherArmy.foeEntities) - this.addFoe(gameState, id, true); - // TODO: reassign those ? - for (let id of otherArmy.ownEntities) - this.addOwn(gameState, id, true); - - this.recalculatePosition(gameState, true); - this.recalculateStrengths(gameState); - - return true; -}; - -m.DefenseArmy.prototype.needsDefenders = function(gameState) -{ - let defenseRatio; - let 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. */ -m.DefenseArmy.prototype.recalculatePosition = function(gameState, force) -{ - if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) - return; - - let npos = 0; - let pos = [0, 0]; - for (let id of this.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - npos++; - let epos = ent.position(); - pos[0] += epos[0]; - pos[1] += epos[1]; - } - // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position - if (npos > 0) - { - this.foePosition[0] = pos[0]/npos; - this.foePosition[1] = pos[1]/npos; - } - - this.positionLastUpdate = gameState.ai.elapsedTime; -}; - -m.DefenseArmy.prototype.recalculateStrengths = function(gameState) -{ - this.ownStrength = 0; - this.foeStrength = 0; - - for (let id of this.foeEntities) - this.evaluateStrength(gameState.getEntityById(id)); - for (let id of this.ownEntities) - this.evaluateStrength(gameState.getEntityById(id), true); -}; - -/** adds or remove the strength of the entity either to the enemy or to our units. */ -m.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 = m.getMaxStrength(ent); - - // TODO adapt the getMaxStrength function for animals. - // For the time being, just increase it for elephants as the returned value is too small. - if (ent.hasClass("Animal") && ent.hasClass("Elephant")) - entStrength *= 3; - - if (remove) - entStrength *= -1; - - if (isOwn) - this.ownStrength += entStrength; - else - this.foeStrength += entStrength; -}; - -m.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 (let evt of events.EntityRenamed) // take care of promoted and packed units - { - if (this.foeEntities.indexOf(evt.entity) !== -1) - { - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed - continue; - let idx = this.foeEntities.indexOf(evt.entity); - this.foeEntities[idx] = evt.newentity; - this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; - this.assignedAgainst[evt.entity] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] === evt.entity) - this.assignedTo[to] = evt.newentity; - } - else if (this.ownEntities.indexOf(evt.entity) !== -1) - { - let idx = this.ownEntities.indexOf(evt.entity); - this.ownEntities[idx] = evt.newentity; - this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; - this.assignedTo[evt.entity] = undefined; - for (let against in this.assignedAgainst) - { - if (!this.assignedAgainst[against]) - continue; - if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) - this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; - } - } - } - - for (let evt of events.Garrison) - this.removeFoe(gameState, evt.entity); - - for (let evt of events.OwnershipChanged) // captured - { - if (!gameState.isPlayerEnemy(evt.to)) - this.removeFoe(gameState, evt.entity); - else if (evt.from === PlayerID) - this.removeOwn(gameState, evt.entity); - } - - for (let evt of events.Destroy) - { - let entityObj = evt.entityObj || undefined; - // we may have capture+destroy, so do not trust owner and check all possibilities - this.removeOwn(gameState, evt.entity, entityObj); - this.removeFoe(gameState, evt.entity, entityObj); - } -}; - -m.DefenseArmy.prototype.update = function(gameState) -{ - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent) - continue; - let 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") - { - let target = gameState.getEntityById(orderData[0].target); - if (target && !m.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) - let 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 []; - } - - let breakaways = []; - // TODO: assign unassigned defenders, cleanup of a few things. - // perhaps occasional strength recomputation - - // occasional update or breakaways, positions… - if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) - { - this.recalculatePosition(gameState); - this.positionLastUpdate = gameState.ai.elapsedTime; - - // Check for breakaways. - for (let i = 0; i < this.foeEntities.length; ++i) - { - let id = this.foeEntities[i]; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) - { - breakaways.push(id); - if (this.removeFoe(gameState, id)) - i--; - } - } - - this.recalculatePosition(gameState); - } - - return breakaways; -}; - -m.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 - }; -}; - -m.DefenseArmy.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/defenseManager.js b/install/petraBased/petra-patriot/defenseManager.js deleted file mode 100644 index 0d18897..0000000 --- a/install/petraBased/petra-patriot/defenseManager.js +++ /dev/null @@ -1,957 +0,0 @@ -var PETRA = function(m) -{ - -m.DefenseManager = function(Config) -{ - this.armies = []; // array of "army" Objects - 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 = {}; -}; - -m.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) - { - let 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 = {}; - let attackingArmies = clone(this.attackingArmies); - for (let enemy in this.attackingUnits) - { - if (!this.attackingUnits[enemy]) - continue; - for (let ally in this.attackingUnits[enemy]) - { - 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 (let enemy in attackingArmies) - { - for (let 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(); -}; - -m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") -{ - if (type == "default") - { - // Try to add it to an existing army. - for (let army of this.armies) - if (army.getType() == type && army.addFoe(gameState, entityID)) - return; // over - } - - // Create a new army for it. - let army = new m.DefenseArmy(gameState, [entityID], type); - - this.armies.push(army); -}; - -m.DefenseManager.prototype.getArmy = function(partOfArmy) -{ - // Find the army corresponding to this ID partOfArmy - for (let army of this.armies) - if (army.ID == partOfArmy) - return army; - - return undefined; -}; - -m.DefenseManager.prototype.isDangerous = function(gameState, entity) -{ - if (!entity.position()) - return false; - - let 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") - { - let targetId = entity.unitAIOrderData()[0].target; - if (this.targetList.indexOf(targetId) != -1) - return true; - let target = gameState.getEntityById(targetId); - if (target) - { - let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); - if (isTargetEnemy && territoryOwner == PlayerID) - { - if (target.hasClass("Structure")) - this.targetList.push(targetId); - return true; - } - else if (isTargetEnemy && target.hasClass("CivCentre")) - { - let myBuildings = gameState.getOwnStructures(); - for (let building of myBuildings.values()) - { - if (building.foundationProgress() == 0) - continue; - if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) - continue; - this.targetList.push(targetId); - return true; - } - } - } - } - - 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 (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position()) // the enemy base is either destroyed or built - continue; - if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) - return true; - } - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) - continue; - let cooperation = this.GetCooperationLevel(cc.owner()); - if (cooperation < 0.6 && cc.foundationProgress() !== undefined) - continue; - if (cooperation < 0.3) - continue; - if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) - return true; - } - - for (let building of gameState.getOwnStructures().values()) - { - if (building.foundationProgress() == 0 || - API3.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 (let building of gameState.getAllyStructures(territoryOwner).values()) - { - if (building.foundationProgress() == 0 || - API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) - continue; - if (!this.territoryMap.isBlinking(building.position())) - return true; - } - } - - // Update the number of enemies attacking this ally - let 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; -}; - -m.DefenseManager.prototype.checkEnemyUnits = function(gameState) -{ - const nbPlayers = gameState.sharedScript.playersData.length; - let i = gameState.ai.playedTurn % nbPlayers; - this.attackingUnits[i] = undefined; - - if (i == PlayerID) - { - if (!this.armies.length) - { - // check if we can recover capture points from any of our notdecaying structures - for (let ent of gameState.getOwnStructures().values()) - { - if (ent.decaying()) - continue; - let 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; - - // loop through enemy units - for (let 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; - let orders = ent.unitAIOrderData(); - if (!orders || !orders.length || !orders[0].target) - continue; - let target = gameState.getEntityById(orders[0].target); - if (!target || !gameState.isPlayerAlly(target.owner())) - continue; - } - - // TODO what to do for ships ? - if (ent.hasClass("Ship") || ent.hasClass("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.numActiveBases() == 0) - 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 (let ent of gameState.getEnemyStructures(i).values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - continue; - if (!ent.capturePoints() && !ent.hasDefensiveFire()) - continue; - let owner = this.territoryMap.getOwner(ent.position()); - if (owner == PlayerID) - this.makeIntoArmy(gameState, ent.id(), "capturing"); - } -}; - -m.DefenseManager.prototype.checkEnemyArmies = function(gameState) -{ - for (let i = 0; i < this.armies.length; ++i) - { - let army = this.armies[i]; - // this returns a list of IDs: the units that broke away from the army for being too far. - let breakaways = army.update(gameState); - for (let breaker of breakaways) - this.makeIntoArmy(gameState, breaker); // assume dangerosity - - if (army.getState() == 0) - { - if (army.getType() == "default") - this.switchToAttack(gameState, army); - army.clear(gameState); - this.armies.splice(i--, 1); - continue; - } - } - // Check if we can't merge it with another - for (let i = 0; i < this.armies.length - 1; ++i) - { - let army = this.armies[i]; - if (army.getType() != "default") - continue; - for (let j = i+1; j < this.armies.length; ++j) - { - let otherArmy = this.armies[j]; - if (otherArmy.getType() != "default" || - API3.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) - { - let army = this.armies[i]; - army.recalculatePosition(gameState); - let owner = this.territoryMap.getOwner(army.foePosition); - if (!gameState.isPlayerEnemy(owner)) - { - if (gameState.isPlayerMutualAlly(owner)) - { - // update the number of enemies attacking this ally - for (let id of army.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent) - continue; - let 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; - } - else if (owner != 0) // enemy army back in its territory - { - 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; - let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let base of bases.values()) - { - if (!gameState.isEntityAlly(base)) - continue; - let cooperation = this.GetCooperationLevel(base.owner()); - if (cooperation < 0.3 && !gameState.isEntityOwn(base)) - continue; - if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) - continue; - if(this.Config.debug > 1) - API3.warn("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 (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (API3.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); - } -}; - -m.DefenseManager.prototype.assignDefenders = function(gameState) -{ - if (!this.armies.length) - return; - - let armiesNeeding = []; - // let's add defenders - for (let army of this.armies) - { - let needsDef = army.needsDefenders(gameState); - if (needsDef === false) - continue; - - let armyAccess; - for (let entId of army.foeEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - armyAccess = m.getLandAccess(gameState, ent); - break; - } - if (!armyAccess) - API3.warn(" 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 - let 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.hasClass("Catapult")) - return; - if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) - 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || 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) - { - let ent = gameState.getEntityById(potentialDefenders[i]); - if (!ent || !ent.position()) - continue; - let aMin; - let distMin; - let access = ipass == 0 ? m.getLandAccess(gameState, ent) : undefined; - for (let a = 0; a < armiesNeeding.length; ++a) - { - if (access && armiesNeeding[a].access != access) - continue; - let dist = API3.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 -= m.getMaxStrength(ent); - 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 centre - let armiesPos = []; - for (let a = 0; a < armiesNeeding.length; ++a) - armiesPos.push(armiesNeeding[a].army.foePosition); - gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); -}; - -m.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 - */ -m.DefenseManager.prototype.checkEvents = function(gameState, events) -{ - // must be called every turn for all armies - for (let army of this.armies) - army.checkEvents(gameState, events); - - for (let evt of events.OwnershipChanged) // capture events - { - if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) - { - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("CivCentre")) // one of our cc has been captured - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); - } - } - - let allAttacked = {}; - for (let evt of events.Attacked) - allAttacked[evt.target] = evt.attacker; - - for (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position()) - continue; - - let 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")) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (target.isIdle() || unitAIStateOrder == "GATHER") - { - let pos = attacker.position(); - let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; - if (range * range > API3.SquareVectorDistance(pos, target.position())) - target.moveToRange(pos[0], pos[1], range, range); - } - continue; - } - - if (target.hasClass("Ship")) // TODO integrate other ships later, need to be sure it is accessible - 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")) - { - let 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)) - { - let capture = target.capturePoints(); - if (!capture) - continue; - let 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 - let plan = target.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && attack.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.hasClass("Structure") && !attacker.hasClass("Ship")) - { - let territoryOwner = this.territoryMap.getOwner(attacker.position()); - if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) - this.makeIntoArmy(gameState, attacker.id()); - } - - if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) - { - let 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; - let 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 healthlevel - 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 catapult - if (target.hasClass("Catapult") && - !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 if we should retaliate or continue our task - if (target.hasClass("Support") || target.attackTypes() === undefined) - continue; - let orderData = target.unitAIOrderData(); - let currentTarget = orderData && orderData.length && orderData[0].target ? - gameState.getEntityById(orderData[0].target) : undefined; - if (currentTarget) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || - !currentTarget.hasClass("Structure") && !currentTarget.hasClass("Support"))) - continue; - if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) - continue; - if (unitAIStateOrder == "COMBAT" && !m.isSiegeUnit(currentTarget) && - gameState.ai.HQ.capturableTargets.has(orderData[0].target)) - { - // take the nearest unit also attacking this structure to help us - let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); - let minDist; - let minEnt; - let pos = attacker.position(); - capturableTarget.ents.delete(target.id()); - for (let entId of capturableTarget.ents) - { - if (allAttacked[entId]) - continue; - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - // Check that the unit is still attacking the structure (since the last played turn) - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let entOrderData = ent.unitAIOrderData(); - if (!entOrderData || !entOrderData.length || !entOrderData[0].target || - entOrderData[0].target != orderData[0].target) - continue; - let dist = API3.SquareVectorDistance(pos, ent.position()); - if (minEnt && dist > minDist) - continue; - minDist = dist; - minEnt = ent; - } - if (minEnt) - { - capturableTarget.ents.delete(minEnt.id()); - minEnt.attack(attacker.id(), m.allowCapture(gameState, minEnt, attacker)); - } - } - } - target.attack(attacker.id(), m.allowCapture(gameState, target, attacker)); - } - } -}; - -m.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) -{ - if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) - return false; - let minGarrison = data.min || target.garrisonMax(); - if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - return false; - if (data.attacker) - { - let attackTypes = target.attackTypes(); - if (!attackTypes || attackTypes.indexOf("Ranged") == -1) - return false; - let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); - let range = target.attackRange("Ranged").max; - if (dist >= range*range) - return false; - } - let access = m.getLandAccess(gameState, target); - let garrisonManager = gameState.ai.HQ.garrisonManager; - let garrisonArrowClasses = target.getGarrisonArrowClasses(); - let typeGarrison = data.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") : !m.isSiegeUnit(data.attacker); - else - allowMelee = true; - } - let units = gameState.getOwnUnits().filter(ent => { - if (!ent.position()) - return false; - if (!MatchesClassList(ent.classes(), garrisonArrowClasses)) - return false; - if (typeGarrison != "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) - return false; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return false; - let 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) - { - let 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 != "decay" && subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(target.position()); - - let ret = false; - for (let ent of units.values()) - { - if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - break; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - let 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; -}; - -/** garrison a attacked siege ranged unit inside the nearest fortress */ -m.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (nearest) - garrisonManager.garrison(gameState, unit, nearest, "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 - */ -m.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!emergency && !ent.buffHeal()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && - (!emergency || !ent.garrisoned().length)) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (!nearest) - return false; - - if (!emergency) - { - garrisonManager.garrison(gameState, unit, nearest, "protection"); - return true; - } - if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent - nearest.unload(nearest.garrisoned()[0]); - - garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); - return true; -}; - -/** - * Be more inclined to help an ally attacked by several enemies - */ -m.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 - */ -m.DefenseManager.prototype.switchToAttack = function(gameState, army) -{ - if (!army) - return; - for (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) - continue; - let targetAccess = m.getLandAccess(gameState, target); - let targetPos = target.position(); - for (let entId of army.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position() || m.getLandAccess(gameState, ent) != targetAccess) - continue; - if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) - continue; - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); - return; - } - } -}; - -m.DefenseManager.prototype.Serialize = function() -{ - let properties = { - "targetList": this.targetList, - "armyMergeSize": this.armyMergeSize, - "attackingUnits": this.attackingUnits, - "attackingArmies": this.attackingArmies, - "attackedAllies": this.attackedAllies - }; - - let armies = []; - for (let army of this.armies) - armies.push(army.Serialize()); - - return { "properties": properties, "armies": armies }; -}; - -m.DefenseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.armies = []; - for (let dataArmy of data.armies) - { - let army = new m.DefenseArmy(gameState, []); - army.Deserialize(dataArmy); - this.armies.push(army); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/diplomacyManager.js b/install/petraBased/petra-patriot/diplomacyManager.js deleted file mode 100644 index 5b8e259..0000000 --- a/install/petraBased/petra-patriot/diplomacyManager.js +++ /dev/null @@ -1,559 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the diplomacy: - * update our cooperative trait - * sent tribute to allies - * decide which player to turn against in "Last Man Standing" mode - * respond to diplomacy requests - * send diplomacy requests to other players (rarely) - */ - -/** - * If a player sends us an ally or neutral request, an Object in this.receivedDiplomacyRequests will be created - * that includes the request status, and the amount and type of the resource tribute (if any) - * that they must send in order for us to accept their request. - * In addition, a message will be sent if the player has not sent us a tribute within a minute. - * If two minutes pass without a tribute, we will decline their request. - * - * If we send a diplomacy request to another player, an Object in this.sentDiplomacyRequests will be created, - * which consists of the requestType (i.e. "ally" or "neutral") and the timeSent. A chat message will be sent - * to the other player, and AI players will actually be informed of the request by a DiplomacyRequest event - * sent through AIInterface. It is expected that the other player will change their diplomacy stance to the stance - * that we suggested within a period of time, or else the request will be deleted from this.sentDiplomacyRequests. - */ -m.DiplomacyManager = function(Config) -{ - 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 = 120 + randFloat(10, 100); -}; - -/** - * 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. - */ -m.DiplomacyManager.prototype.init = function(gameState) -{ - this.lastManStandingCheck(gameState); - - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - 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 - */ -m.DiplomacyManager.prototype.tributes = function(gameState) -{ - this.nextTributeUpdate = gameState.ai.elapsedTime + 30; - let totalResources = gameState.getResources(); - let 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; - let donor = gameState.getAlliedVictory() || gameState.getEntities(i).length < gameState.getOwnEntities().length; - let allyResources = gameState.sharedScript.playersData[i].resourceCounts; - let allyPop = gameState.sharedScript.playersData[i].popCount; - let tribute = {}; - let toSend = false; - for (let res in allyResources) - { - 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); - for (let k = 0; k < 2; ++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); - m.chatRequestTribute(gameState, res); - if (this.Config.debug > 1) - API3.warn("Tribute on " + res + " requested to player " + i); - break; - } - } - } - } - if (!toSend) - continue; - if (this.Config.debug > 1) - API3.warn("Tribute " + uneval(tribute) + " sent to player " + i); - if (this.Config.chat) - m.chatSentTribute(gameState, i); - Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute }); - } -}; - -m.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 (let evt of events.TributeExchanged) - { - if (evt.to === PlayerID && !gameState.isPlayerAlly(evt.from) && this.receivedDiplomacyRequests.has(evt.from)) - { - let request = this.receivedDiplomacyRequests.get(evt.from); - if (request.status === "waitingForTribute") - { - request.wanted -= evt.amounts[request.type]; - - if (request.wanted <= 0) - { - if (this.Config.debug > 1) - API3.warn("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 (let 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 (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || - gameState.ai.HQ.territoryMap.getOwner(target.position()) !== PlayerID || - !gameState.isPlayerEnemy(target.owner())) - continue; - let 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 (let 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 - { - let 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; - } - - let 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)) - { - let response = request !== undefined && (request.status === "declinedRequest" || request.status === "allianceBroken") ? - "decline" : "declineSuggestNeutral"; - m.chatAnswerRequestDiplomacy(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 (let evt of events.DiplomacyRequest) - { - if (evt.player !== PlayerID) - continue; - - this.handleDiplomacyRequest(gameState, evt.source, evt.to); - let request = this.receivedDiplomacyRequests.get(evt.source); - if (this.Config.debug > 0) - API3.warn("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 - }); - } - } - - // An AI player we sent a diplomacy request to demanded we send them a tribute - for (let evt of events.TributeRequest) - { - if (evt.player !== PlayerID) - continue; - - let 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 responseTribute = {}; - responseTribute[evt.resourceType] = evt.resourceWanted; - if (this.Config.debug > 0) - API3.warn("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; - } - } -}; - -/** - * 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. - */ -m.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 turnFactor = 0; - 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; - - 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")) - { - let wonder = gameState.getEnemyStructures(i).filter(API3.Filters.byClass("Wonder"))[0]; - if (wonder) - { - let wonderProgess = wonder.foundationProgress(); - if (wonderProgess === undefined) - { - playerToTurnAgainst = i; - break; - } - turnFactor += wonderProgess * 2.5 + this.betrayWeighting; - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - let relicsCount = gameState.updatingGlobalCollection("allRelics", API3.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"); - let request = this.receivedDiplomacyRequests.get(playerToTurnAgainst); - if (request && request.status !== "allianceBroken") - { - if (request.status === "waitingForTribute") - m.chatAnswerRequestDiplomacy(gameState, player, 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. - */ -m.DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player, requestType) -{ - if (gameState.sharedScript.playersData[PlayerID].teamsLocked) - return; - let response; - let requiredTribute; - let request = this.receivedDiplomacyRequests.get(player); - let 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"; - } - 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; - } - } - 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 - { - response = "acceptWithTribute"; - requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState)[0]; - 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 - }); - } - m.chatAnswerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); -}; - -m.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) - API3.warn("diplomacy stance with player " + player + " is now " + newDiplomaticStance); - if (this.Config.chat) - m.chatNewDiplomacy(gameState, player, newDiplomaticStance); -}; - -m.DiplomacyManager.prototype.checkRequestedTributes = function(gameState) -{ - for (let [player, data] of this.receivedDiplomacyRequests) - if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime) - { - if (data.sentWarning) - { - this.receivedDiplomacyRequests.delete(player); - m.chatAnswerRequestDiplomacy(gameState, player, data.requestType, "decline"); - } - else - { - data.sentWarning = true; - data.warnTime = gameState.ai.elapsedTime + 60; - m.chatAnswerRequestDiplomacy(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. - */ -m.DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState) -{ - let player; - let max = 0; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - let mutualEnemies = 0; - let 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; - - 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; - - let requestType = gameState.isPlayerNeutral(player) ? "ally" : "neutral"; - - this.sentDiplomacyRequests.set(player, { - "requestType": requestType, - "timeSent": gameState.ai.elapsedTime - }); - - if (this.Config.debug > 0) - API3.warn("Sending diplomacy request to player " + player + " with " + requestType); - Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType }); - m.chatNewRequestDiplomacy(gameState, player, requestType, "sendRequest"); -}; - -m.DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState) -{ - for (let [player, data] of this.sentDiplomacyRequests) - if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources && - gameState.getPopulation() > 70) - { - m.chatNewRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); - this.sentDiplomacyRequests.delete(player); - } -}; - -m.DiplomacyManager.prototype.update = function(gameState, events) -{ - this.checkEvents(gameState, events); - - if (!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) - { - this.sentDiplomacyRequestLapseTime = gameState.ai.elapsedTime + 300 + randFloat(10, 100); - let 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); -}; - -m.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 - }; -}; - -m.DiplomacyManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/entityExtend.js b/install/petraBased/petra-patriot/entityExtend.js deleted file mode 100644 index 3edbdbf..0000000 --- a/install/petraBased/petra-patriot/entityExtend.js +++ /dev/null @@ -1,441 +0,0 @@ -var PETRA = function(m) -{ - -/** returns true if this unit should be considered as a siege unit */ -m.isSiegeUnit = function(ent) -{ - return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee") && ent.hasClass("Champion"); -}; - -/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ -m.getMaxStrength = function(ent, againstClass) -{ - let strength = 0; - let attackTypes = ent.attackTypes(); - if (!attackTypes) - return strength; - - for (let type of attackTypes) - { - if (type == "Slaughter") - continue; - - let attackStrength = ent.attackStrengths(type); - for (let str in attackStrength) - { - let val = parseFloat(attackStrength[str]); - if (againstClass) - val *= ent.getMultiplierAgainst(type, againstClass); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength"); - } - } - - let attackRange = ent.attackRange(type); - if (attackRange) - strength += attackRange.max * 0.0125; - - let attackTimes = ent.attackTimes(type); - for (let str in attackTimes) - { - let val = parseFloat(attackTimes[str]); - switch (str) - { - case "repeat": - strength += val / 100000; - break; - case "prepare": - strength -= val / 100000; - break; - default: - API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); - } - } - } - - let armourStrength = ent.armourStrengths(); - for (let str in armourStrength) - { - let val = parseFloat(armourStrength[str]); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength"); - } - } - - return strength * ent.maxHitpoints() / 100.0; -}; - -/** Get access and cache it (except for units as it can change) in metadata if not already done */ -m.getLandAccess = function(gameState, ent) -{ - if (ent.hasClass("Unit")) - return gameState.ai.accessibility.getAccessValue(ent.position()); - - let access = ent.getMetadata(PlayerID, "access"); - if (!access) - { - access = gameState.ai.accessibility.getAccessValue(ent.position()); - // Docks are sometimes not as expected - if (access < 2 && ent.buildPlacementType() == "shore") - { - let halfDepth = 0; - if (ent.get("Footprint/Square")) - halfDepth = +ent.get("Footprint/Square/@depth") / 2; - else if (ent.get("Footprint/Circle")) - halfDepth = +ent.get("Footprint/Circle/@radius"); - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < halfDepth; d += 3) - { - let pos = [ entPos[0] - d * sina, - entPos[1] - d * cosa]; - access = gameState.ai.accessibility.getAccessValue(pos); - if (access > 1) - break; - } - } - ent.setMetadata(PlayerID, "access", access); - } - return access; -}; - -/** Sea access always cached as it never changes */ -m.getSeaAccess = function(gameState, ent) -{ - let sea = ent.getMetadata(PlayerID, "sea"); - if (!sea) - { - sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); - // Docks are sometimes not as expected - if (sea < 2 && ent.buildPlacementType() == "shore") - { - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < 15; d += 3) - { - let pos = [ entPos[0] + d * sina, - entPos[1] + d * cosa]; - sea = gameState.ai.accessibility.getAccessValue(pos, true); - if (sea > 1) - break; - } - } - ent.setMetadata(PlayerID, "sea", sea); - } - return sea; -}; - -m.setSeaAccess = function(gameState, ent) -{ - m.getSeaAccess(gameState, ent); -}; - -/** Decide if we should try to capture (returns true) or destroy (return false) */ -m.allowCapture = function(gameState, ent, target) -{ - if (!target.isCapturable() || !ent.canCapture(target)) - return false; - if (target.isInvulnerable()) - return true; - // always try to recapture cp from an allied, except if it's decaying - if (gameState.isPlayerAlly(target.owner())) - return !target.decaying(); - - let antiCapture = target.defaultRegenRate(); - if (target.isGarrisonHolder() && target.garrisoned()) - antiCapture += target.garrisonRegenRate() * target.garrisoned().length; - if (target.decaying()) - antiCapture -= target.territoryDecayRate(); - - let capture; - let capturableTargets = gameState.ai.HQ.capturableTargets; - if (!capturableTargets.has(target.id())) - { - capture = ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); - } - else - { - let capturable = capturableTargets.get(target.id()); - if (!capturable.ents.has(ent.id())) - { - capturable.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturable.ents.add(ent.id()); - } - capture = capturable.strength; - } - capture *= 1 / (0.1 + 0.9*target.healthLevel()); - let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); - if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) - return capture > antiCapture + sumCapturePoints/50; - return capture > antiCapture + sumCapturePoints/80; -}; - -m.getAttackBonus = function(ent, target, type) -{ - let attackBonus = 1; - if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) - return attackBonus; - let bonuses = ent.get("Attack/" + type + "/Bonuses"); - for (let key in bonuses) - { - let bonus = bonuses[key]; - if (bonus.Civ && bonus.Civ !== target.civ()) - continue; - if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) - continue; - attackBonus *= bonus.Multiplier; - } - return attackBonus; -}; - -/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ -m.returnResources = function(gameState, ent) -{ - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) - return false; - - let resource = ent.resourceCarrying()[0].type; - - let closestDropsite; - let distmin = Math.min(); - let access = m.getLandAccess(gameState, ent); - let dropsiteCollection = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); - for (let dropsite of dropsiteCollection.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again - if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - closestDropsite = dropsite; - } - - if (!closestDropsite) - return false; - ent.returnResources(closestDropsite); - return true; -}; - -/** is supply full taking into account gatherers affected during this turn */ -m.IsSupplyFull = function(gameState, ent) -{ - return ent.isFull() === true || - ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); -}; - -/** - * Get the best base (in terms of distance and accessIndex) for an entity. - * It should be on the same accessIndex for structures. - * If nothing found, return the base[0] for units and undefined for structures. - * If exclude is given, we exclude the base with ID = exclude. - */ -m.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) -{ - let pos = ent.position(); - let accessIndex; - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - { - API3.warn("Petra error: entity without position, but not garrisoned"); - m.dumpEntity(ent); - return gameState.ai.HQ.baseManagers[0]; - } - pos = holder.position(); - accessIndex = m.getLandAccess(gameState, holder); - } - else - accessIndex = m.getLandAccess(gameState, ent); - - let distmin = Math.min(); - let dist; - let bestbase; - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) - continue; - if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) - continue; - if (ent.hasClass("Structure") && base.accessIndex != accessIndex) - continue; - if (base.anchor && base.anchor.position()) - dist = API3.SquareVectorDistance(base.anchor.position(), pos); - else - { - let found = false; - for (let structure of base.buildings.values()) - { - if (!structure.position()) - continue; - dist = API3.SquareVectorDistance(structure.position(), pos); - found = true; - break; - } - if (!found) - continue; - } - if (base.accessIndex != accessIndex) - dist += 50000000; - if (!base.anchor) - dist += 50000000; - if (dist > distmin) - continue; - distmin = dist; - bestbase = base; - } - if (!bestbase && !ent.hasClass("Structure")) - bestbase = gameState.ai.HQ.baseManagers[0]; - return bestbase; -}; - -m.getHolder = function(gameState, ent) -{ - for (let holder of gameState.getEntities().values()) - { - if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) - return holder; - } - return undefined; -}; - -/** return the template of the built foundation if a foundation, otherwise return the entity itself */ -m.getBuiltEntity = function(gameState, ent) -{ - if (ent.foundationProgress() !== undefined) - return gameState.getBuiltTemplate(ent.templateName()); - - return ent; -}; - -/** - * return true if it is not worth finishing this building (it would surely decay) - * TODO implement the other conditions - */ -m.isNotWorthBuilding = function(gameState, ent) -{ - if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) - { - let buildTerritories = ent.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) - return true; - } - return false; -}; - -/** - * Check if the straight line between the two positions crosses an enemy territory - */ -m.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) -{ - let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; - let stepx = (pos2[0] - pos1[0]) / n; - let stepy = (pos2[1] - pos1[1]) / n; - for (let i = 1; i < n; ++i) - { - let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; - let owner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (owner && gameState.isPlayerEnemy(owner)) - return true; - } - return false; -}; - -m.gatherTreasure = function(gameState, ent, water = false) -{ - if (!gameState.ai.HQ.treasures.hasEntities()) - return false; - if (!ent || !ent.position()) - return false; - let rates = ent.resourceGatherRates(); - if (!rates || !rates.treasure || rates.treasure <= 0) - return false; - let treasureFound; - let distmin = Math.min(); - let access = water ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - for (let treasure of gameState.ai.HQ.treasures.values()) - { - if (m.IsSupplyFull(gameState, treasure)) - continue; - // let some time for the previous gatherer to reach the treasure before trying again - let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); - if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) - continue; - if (!water && access != m.getLandAccess(gameState, treasure)) - continue; - if (water && access != m.getSeaAccess(gameState, treasure)) - continue; - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - continue; - let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); - if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit - continue; - if (dist > distmin) - continue; - distmin = dist; - treasureFound = treasure; - } - if (!treasureFound) - return false; - treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); - ent.gather(treasureFound); - gameState.ai.HQ.AddTCGatherer(treasureFound.id()); - ent.setMetadata(PlayerID, "supply", treasureFound.id()); - return true; -}; - -m.dumpEntity = function(ent) -{ - if (!ent) - return; - API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + - " state " + ent.unitAIState()); - API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + - " subrole " + ent.getMetadata(PlayerID, "subrole")); - API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + - " foundationProgress " + ent.foundationProgress()); - API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + - " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + - " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); - API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + - " gather-type " + ent.getMetadata(PlayerID, "gather-type") + - " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + - " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/garrisonManager.js b/install/petraBased/petra-patriot/garrisonManager.js deleted file mode 100644 index c9ec57c..0000000 --- a/install/petraBased/petra-patriot/garrisonManager.js +++ /dev/null @@ -1,373 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the garrisonHolders - * When a unit is ordered to garrison, it must be done through this.garrison() function so that - * an object in this.holders is created. This object contains an array with the entities - * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). - * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) - */ - -m.GarrisonManager = function(Config) -{ - this.Config = Config; - this.holders = new Map(); - this.decayingStructures = new Map(); -}; - -m.GarrisonManager.prototype.update = function(gameState, events) -{ - // First check for possible upgrade of a structure - for (let evt of events.EntityRenamed) - { - for (let id of this.holders.keys()) - { - if (id != evt.entity) - continue; - let data = this.holders.get(id); - let newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) - { - this.holders.delete(id); - this.holders.set(evt.newentity, data); - } - else - { - for (let entId of data.list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - continue; - this.leaveGarrison(ent); - ent.stopMoving(); - } - this.holders.delete(id); - } - } - - for (let id of this.decayingStructures.keys()) - { - if (id !== evt.entity) - continue; - this.decayingStructures.delete(id); - if (this.decayingStructures.has(evt.newentity)) - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) - continue; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(evt.newentity, gmin); - } - } - - for (let [id, data] of this.holders.entries()) - { - let list = data.list; - let holder = gameState.getEntityById(id); - if (!holder || !gameState.isPlayerAlly(holder.owner())) - { - // this holder was certainly destroyed or captured. Let's remove it - for (let entId of list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - 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 (let evt of events.EntityRenamed) - if (evt.entity === list[j]) - list[j] = evt.newentity; - - let ent = gameState.getEntityById(list[j]); - if (!ent) // unit must have been killed while garrisoning - list.splice(j--, 1); - else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned - { - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (ent.unitAIOrderData().some(order => order.target && order.target == id)) - continue; - if (ent.getMetadata(PlayerID, "garrisonHolder") == id) - { - // The garrison order must have failed - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (gameState.ai.Config.debug > 0) - { - API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + - ") is expected to garrison in " + id + " (" + holder.genericName() + - "), but has no such garrison order " + uneval(ent.unitAIOrderData())); - m.dumpEntity(ent); - } - list.splice(j--, 1); - } - } - - } - - if (!holder.position()) // could happen with siege unit inside a ship - continue; - - if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) - { - let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; - let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; - for (let 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; - let dist = API3.SquareVectorDistance(ent.position(), holder.position()); - if (dist > range*range) - continue; - if (ent.hasClass("Structure")) - around.defenseStructure = true; - else if (m.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 (let entId of holder.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) - holder.unload(entId); - } - for (let j = 0; j < list.length; ++j) - { - let 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.numberOfGarrisonedUnits(holder) === 0) - this.holders.delete(id); - else - holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); - } - } - - // Warning new garrison orders (as in the following lines) should be done after having updated the holders - // (or TODO we should add a test that the garrison order is from a previous turn when updating) - for (let [id, gmin] of this.decayingStructures.entries()) - { - let ent = gameState.getEntityById(id); - if (!ent || ent.owner() !== PlayerID) - this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedUnits(ent) < gmin) - gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); - } -}; - -/** TODO should add the units garrisoned inside garrisoned units */ -m.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) -{ - if (!this.holders.has(holder.id())) - return holder.garrisoned().length; - - return holder.garrisoned().length + this.holders.get(holder.id()).list.length; -}; - -m.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 */ -m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) -{ - if (this.numberOfGarrisonedUnits(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")); - } - - if (ent.getMetadata(PlayerID, "plan") !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - ent.setMetadata(PlayerID, "subrole", "garrisoning"); - ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); - ent.setMetadata(PlayerID, "garrisonType", type); - ent.garrison(holder); -}; - -/** - This is the end of the pre-garrison state, either because the entity is really garrisoned - or because it has changed its order (i.e. because the garrisonHolder was destroyed) - 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 - */ -m.GarrisonManager.prototype.leaveGarrison = function(ent) -{ - ent.setMetadata(PlayerID, "subrole", undefined); - if (ent.getMetadata(PlayerID, "plan") === -2) - ent.setMetadata(PlayerID, "plan", -1); - else - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "garrisonHolder", undefined); -}; - -/** Cancel a pre-garrison state */ -m.GarrisonManager.prototype.cancelGarrison = function(ent) -{ - ent.stopMoving(); - this.leaveGarrison(ent); - let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); - if (!holderId || !this.holders.has(holderId)) - return; - let list = this.holders.get(holderId).list; - let index = list.indexOf(ent.id()); - if (index !== -1) - list.splice(index, 1); -}; - -m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) -{ - switch (ent.getMetadata(PlayerID, "garrisonType")) - { - case 'force': // force the ungarrisoning - return false; - case 'trade': // trader garrisoned in ship - return true; - case 'protection': // hurt unit for healing or infantry for defense - if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - let capture = ent.capturePoints(); - if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) - return true; - if (MatchesClassList(ent.classes(), 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") || m.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above - if (m.isSiegeUnit(ent)) - return around.meleeSiege; - return holder.buffHeal() && ent.needsHeal(); - case 'decay': - return this.decayingStructures.has(holder.id()); - case '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; - API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + - " for " + ent.genericName() + " id " + ent.id() + - " inside " + holder.genericName() + " id " + holder.id()); - ent.setMetadata(PlayerID, "garrisonType", "protection"); - return true; - } -}; - -/** Add this holder in the list managed by the garrisonManager */ -m.GarrisonManager.prototype.registerHolder = function(gameState, holder) -{ - if (this.holders.has(holder.id())) // already registered - return; - this.holders.set(holder.id(), { "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) - */ -m.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) -{ - if (this.decayingStructures.has(entId)) - return true; - let ent = gameState.getEntityById(entId); - if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) - return false; - if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) - return false; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(entId, gmin); - return true; -}; - -m.GarrisonManager.prototype.removeDecayingStructure = function(entId) -{ - if (!this.decayingStructures.has(entId)) - return; - this.decayingStructures.delete(entId); -}; - -m.GarrisonManager.prototype.Serialize = function() -{ - return { "holders": this.holders, "decayingStructures": this.decayingStructures }; -}; - -m.GarrisonManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/headquarters.js b/install/petraBased/petra-patriot/headquarters.js deleted file mode 100644 index 407e77b..0000000 --- a/install/petraBased/petra-patriot/headquarters.js +++ /dev/null @@ -1,2900 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Headquarters - * Deal with high level logic for the AI. Most of the interesting stuff gets done here. - * Some tasks: - * -defining RESS needs - * -BO decisions. - * > training workers - * > building stuff (though we'll send that to bases) - * -picking strategy (specific manager?) - * -diplomacy -> diplomacyManager - * -planning attacks -> attackManager - * -picking new CC locations. - */ - -m.HQ = function(Config) -{ - this.Config = Config; - this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i - - // Cache various quantities. - this.turnCache = {}; - this.lastFailedGather = {}; - - this.firstBaseConfig = false; - this.currentBase = 0; // Only one base (from baseManager) is run every turn. - - // Workers configuration - this.targetNumWorkers = this.Config.Economy.targetNumWorkers; - this.supportRatio = this.Config.Economy.supportRatio; - - this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime - this.towerStartTime = 0; // stone defense towers, will start as soon as available - this.towerLapseTime = this.Config.Military.towerLapseTime; - this.fortressStartTime = 0; // will start as soon as available - this.fortressLapseTime = this.Config.Military.fortressLapseTime; - this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); - this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); - - this.baseManagers = []; - this.attackManager = new m.AttackManager(this.Config); - this.buildManager = new m.BuildManager(); - this.defenseManager = new m.DefenseManager(this.Config); - this.tradeManager = new m.TradeManager(this.Config); - this.navalManager = new m.NavalManager(this.Config); - this.researchManager = new m.ResearchManager(this.Config); - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.garrisonManager = new m.GarrisonManager(this.Config); - this.victoryManager = new m.VictoryManager(this.Config); - - this.capturableTargets = new Map(); - this.capturableTargetsTime = 0; -}; - -/** More initialisation for stuff that needs the gameState */ -m.HQ.prototype.init = function(gameState, queues) -{ - this.territoryMap = m.createTerritoryMap(gameState); - // initialize base map. Each pixel is a base ID, or 0 if not or not accessible - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - // create borderMap: flag cells on the border of the map - // then this map will be completed with our frontier in updateTerritories - this.borderMap = m.createBorderMap(gameState); - // list of allowed regions - this.landRegions = {}; - // try to determine if we have a water map - this.navalMap = false; - this.navalRegions = {}; - - this.treasures = gameState.getEntities().filter(ent => { - let type = ent.resourceSupplyType(); - return type && type.generic == "treasure"; - }); - this.treasures.registerUpdates(); - this.currentPhase = gameState.currentPhase(); - this.decayingStructures = new Set(); -}; - -/** - * initialization needed after deserialization (only called when deserialization) - */ -m.HQ.prototype.postinit = function(gameState) -{ - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("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 - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - let base = this.getBaseByID(baseID); - base.assignResourceToDropsite(gameState, ent); - } - - this.updateTerritories(gameState); -}; - -/** - * 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 - * allowedType: undefined => new base with an anchor - * "unconstructed" => new base with a foundation anchor - * "captured" => captured base with an anchor - * "anchorless" => anchorless base, currently with dock - */ -m.HQ.prototype.createBase = function(gameState, ent, type) -{ - let access = m.getLandAccess(gameState, ent); - let newbase; - for (let base of this.baseManagers) - { - if (base.accessIndex != access) - continue; - if (type != "anchorless" && base.anchor) - continue; - if (type != "anchorless") - { - // TODO we keep the fisrt 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) - continue; - newbase = base; - if (newbase.anchor) - break; - } - } - - if (this.Config.debug > 0) - { - API3.warn(" ----------------------------------------------------------"); - API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); - API3.warn(" 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 m.BaseManager(gameState, this.Config); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type != "anchorless") - newbase.setAnchor(gameState, ent); - else - newbase.setAnchorlessEntity(gameState, ent); - - return newbase; -}; - -/** - * returns the sea index linking regions 1 and region 2 (supposed to be different land region) - * otherwise return undefined - * for the moment, only the case land-sea-land is supported - */ -m.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) -{ - let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); - if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") - return path[1]; - - if (this.Config.debug > 1) - { - API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); - API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); - API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); - } - return undefined; -}; - -/** TODO check if the new anchorless bases should be added to addBase */ -m.HQ.prototype.checkEvents = function(gameState, events) -{ - let addBase = false; - - this.buildManager.checkEvents(gameState, events); - - if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) - this.updateTerritories(gameState); - - for (let evt of events.DiplomacyChanged) - { - if (evt.player != PlayerID && evt.otherPlayer != PlayerID) - continue; - // Reset the entities collections which depend on diplomacy - gameState.resetOnDiplomacyChanged(); - break; - } - - for (let evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && - evt.metadata[PlayerID].base) - { - let ent = evt.entityObj; - if (ent.owner() != PlayerID) - continue; - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - let base = this.getBaseByID(evt.metadata[PlayerID].base); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState, ent); - } - } - - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (let evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - let newbase = this.createBase(gameState, ent, "unconstructed"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - let newbase = this.createBase(gameState, ent, "anchorless"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } - - for (let evt of events.ConstructionFinished) - { - if (evt.newentity == evt.entity) // repaired building - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID) - continue; - if (ent.hasClass("BarterMarket") && this.maxFields) - this.maxFields = false; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.from == PlayerID) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState, ent); - } - - if (evt.to != PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - if (ent.hasClass("Unit")) - { - m.getBestBase(gameState, ent).assignEntity(gameState, ent); - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - if (ent.hasClass("Trader")) - { - ent.setMetadata(PlayerID, "role", "trader"); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", "worker"); - ent.setMetadata(PlayerID, "subrole", "idle"); - } - if (ent.hasClass("Ship")) - m.setSeaAccess(gameState, ent); - if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, "unconstructed"); - else - { - newbase = this.createBase(gameState, ent, "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, "anchorless"); - else - base = m.getBestBase(gameState, ent) || this.baseManagers[0]; - base.assignEntity(gameState, ent); - if (ent.decaying()) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - } - } - - // deal with the different rally points of training units: the rally point is set when the training starts - // for the time being, only autogarrison is used - - for (let evt of events.TrainingStarted) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) - continue; - let metadata = ent._entity.trainingQueue[0].metadata; - if (metadata && metadata.garrisonType) - ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison - else - ent.unsetRallyPoint(); - } - - for (let evt of events.TrainingFinished) - { - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent.position()) - { - // we are autogarrisoned, check that the holder is registered in the garrisonManager - let holderId = ent.unitAIOrderData()[0].target; - let holder = gameState.getEntityById(holderId); - if (holder) - this.garrisonManager.registerHolder(gameState, holder); - } - else if (ent.getMetadata(PlayerID, "garrisonType")) - { - // we were supposed to be autogarrisoned, but this has failed (may-be full) - ent.setMetadata(PlayerID, "garrisonType", undefined); - } - - // Check if this unit is no more needed in its attack plan - // (happen when the training ends after the attack is started or aborted) - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = this.attackManager.getPlan(plan); - if (!attack || attack.state != "unexecuted") - ent.setMetadata(PlayerID, "plan", -1); - } - // Assign it immediately to something useful to do - if (ent.getMetadata(PlayerID, "role") == "worker") - { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) - { - base = m.getBestBase(gameState, ent); - base.assignEntity(gameState, ent); - } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); - } - else if (ent.resourceSupplyType() && ent.position()) - { - let type = ent.resourceSupplyType(); - if (!type.generic) - continue; - let dropsites = gameState.getOwnDropsites(type.generic); - let pos = ent.position(); - let access = m.getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (let dropsite of dropsites.values()) - { - if (!dropsite.position() || m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); - } - if (goal) - ent.moveToRange(goal[0], goal[1]); - } - } - } - - for (let evt of events.TerritoryDecayChanged) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) - continue; - if (evt.to) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - else if (ent.isGarrisonHolder()) - this.garrisonManager.removeDecayingStructure(evt.entity); - } - - if (addBase) - { - if (!this.firstBaseConfig) - { - // This is our first base, let us configure our starting resources - this.configFirstBase(gameState); - } - else - { - // Let us hope this new base will fix our possible resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } - } - - // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) - if (this.Config.difficulty < 2) - return; - for (let entId of this.decayingStructures) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.decaying() && ent.isOwn(PlayerID)) - { - let capture = ent.capturePoints(); - if (!capture) - continue; - let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); - if (captureRatio < 0.50) - continue; - let decayToGaia = true; - for (let i = 1; i < capture.length; ++i) - { - if (gameState.isPlayerAlly(i) || !capture[i]) - continue; - decayToGaia = false; - break; - } - if (decayToGaia) - continue; - let ratioMax = 0.70 + randFloat(0., 0.1); - for (let evt of events.Attacked) - { - if (ent.id() != evt.target) - continue; - ratioMax = 0.85 + randFloat(0., 0.1); - break; - } - if (captureRatio > ratioMax) - continue; - ent.destroy(); - } - this.decayingStructures.delete(entId); - } -}; - -/** Ensure that all requirements are met when phasing up*/ -m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) -{ - if (gameState.getNumberOfPhases() == this.currentPhase) - return; - - let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); - let plan; - let queue; - for (let entityReq of requirements) - { - // Village requirements are met elsewhere by constructing more houses - if (entityReq.class == "Village" || entityReq.class == "NotField") - continue; - if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) - continue; - switch (entityReq.class) - { - case "Town": - if (!queues.economicBuilding.hasQueuedUnits() && - !queues.militaryBuilding.hasQueuedUnits() && - !queues.defenseBuilding.hasQueuedUnits()) - { - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_market")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_market", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_temple")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_blacksmith")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "phaseUp": true }); - queue = "militaryBuilding"; - break; - } - if (this.canBuild(gameState, "structures/{civ}_defense_tower")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower", { "phaseUp": true }); - queue = "defenseBuilding"; - break; - } - } - break; - default: - // All classes not dealt with inside vanilla game. - // We put them for the time being on the economic queue, except if wonder - queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; - if (!queues[queue].hasQueuedUnits()) - { - let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); - if (structure && this.canBuild(gameState, structure)) - plan = new m.ConstructionPlan(gameState, structure, { "phaseUp": true }); - } - } - - 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); - return; - } - } -}; - -/** Called by any "phase" research plan once it's started */ -m.HQ.prototype.OnPhaseUp = function(gameState, phase) -{ -}; - -/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ -m.HQ.prototype.trainMoreWorkers = function(gameState, queues) -{ - // default template - let requirementsDef = [ ["costsResource", 1, "food"] ]; - let classesDef = ["Support", "Worker"]; - let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); - - // counting the workers that aren't part of a plan - let numberOfWorkers = 0; // all workers - let numberOfSupports = 0; // only support workers (i.e. non fighting) - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - { - ++numberOfWorkers; - if (ent.hasClass("Support")) - ++numberOfSupports; - } - }); - let numberInTraining = 0; - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - numberInTraining += item.count; - if (item.metadata && item.metadata.role && item.metadata.role == "worker" && - item.metadata.plan === undefined) - { - numberOfWorkers += item.count; - if (item.metadata.support) - numberOfSupports += item.count; - } - } - }); - - // Anticipate the optimal batch size when this queue will start - // and adapt the batch size of the first and second queued workers to the present population - // to ease a possible recovery if our population was drastically reduced by an attack - // (need to go up to second queued as it is accounted in queueManager) - let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); - if (queues.villager.plans[0]) - { - queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); - if (queues.villager.plans[1]) - queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); - } - if (queues.citizenSoldier.plans[0]) - { - queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); - if (queues.citizenSoldier.plans[1]) - queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); - } - - let numberOfQueuedSupports = queues.villager.countQueuedUnits(); - let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); - let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; - let numberTotal = numberOfWorkers + numberQueued; - - if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) - return; - if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && - this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) - return; - if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) - return; - - // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers - // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. - // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) - // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers - // and gives a ratio alfa at startup. - - let supportRatio = this.supportRatio; - let alpha = 0.85; - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field"))) - supportRatio = Math.min(this.supportRatio, 0.1); - if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) - alpha = 0.7; - if (gameState.isCeasefireActive()) - alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; - let supportMax = supportRatio * this.targetNumWorkers; - let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); - - let template; - if (!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] ]; - - let classes = ["CitizenSoldier", "Infantry"]; - // We want at least 33% ranged and 33% melee - classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); - - template = this.findBestTrainableUnit(gameState, classes, requirements); - } - - // If the template variable is empty, the default unit (Support unit) will be used - // base "0" means automatic choice of base - if (!template && templateDef) - queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); - else if (template) - queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); -}; - -/** picks the best template based on parameters and classes */ -m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) -{ - let units; - if (classes.indexOf("Hero") != -1) - units = gameState.findTrainableUnits(classes, []); - else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it - units = gameState.findTrainableUnits(classes, ["SiegeTower"]); - else // We do not want hero when not explicitely specified - units = gameState.findTrainableUnits(classes, ["Hero"]); - - if (!units.length) - return undefined; - - let parameters = requirements.slice(); - let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources - for (let type in remainingResources) - { - if (availableResources[type] > 800) - continue; - if (remainingResources[type] > 800) - continue; - let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; - let toAdd = true; - for (let param of parameters) - { - if (param[0] != "costsResource" || param[2] != type) - continue; - param[1] = Math.min(param[1], costsResource); - toAdd = false; - break; - } - if (toAdd) - parameters.push(["costsResource", costsResource, type]); - } - - units.sort((a, b) => { - let aCost = 1 + a[1].costSum(); - let bCost = 1 + b[1].costSum(); - let aValue = 0.1; - let bValue = 0.1; - for (let param of parameters) - { - if (param[0] == "strength") - { - aValue += m.getMaxStrength(a[1]) * param[1]; - bValue += m.getMaxStrength(b[1]) * param[1]; - } - else if (param[0] == "siegeStrength") - { - aValue += m.getMaxStrength(a[1], "Structure") * param[1]; - bValue += m.getMaxStrength(b[1], "Structure") * param[1]; - } - else if (param[0] == "speed") - { - aValue += a[1].walkSpeed() * param[1]; - bValue += b[1].walkSpeed() * param[1]; - } - else if (param[0] == "costsResource") - { - // requires a third parameter which is the resource - if (a[1].cost()[param[2]]) - aValue *= param[1]; - if (b[1].cost()[param[2]]) - bValue *= param[1]; - } - else if (param[0] == "canGather") - { - // checking against wood, could be anything else really. - if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) - aValue *= param[1]; - if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) - bValue *= param[1]; - } - else - API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); - } - return -aValue/aCost + bValue/bCost; - }); - return units[0][0]; -}; - -/** - * returns an entity collection of workers through BaseManager.pickBuilders - * TODO: when same accessIndex, sort by distance - */ -m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) -{ - let accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - // sorting bases by whether they are on the same accessindex or not. - let baseBest = this.baseManagers.slice().sort((a, b) => { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); - - let needed = number; - let workers = new API3.EntityCollection(gameState.sharedScript); - for (let base of baseBest) - { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) - break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; -}; - -m.HQ.prototype.getTotalResourceLevel = function(gameState) -{ - let total = {}; - for (let res of Resources.GetCodes()) - total[res] = 0; - for (let base of this.baseManagers) - for (let res in total) - total[res] += base.getResourceLevel(gameState, res); - - return total; -}; - -/** - * Returns the current gather rate - * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. - */ -m.HQ.prototype.GetCurrentGatherRates = function(gameState) -{ - if (!this.turnCache.currentRates) - { - let currentRates = {}; - for (let res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); - - for (let res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; -}; - -/** - * Returns the wanted gather rate. - */ -m.HQ.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. - */ -m.HQ.prototype.pickMostNeededResources = function(gameState) -{ - let wantedRates = this.GetWantedGatherRates(gameState); - let currentRates = this.GetCurrentGatherRates(gameState); - - let needed = []; - for (let res in wantedRates) - 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 Centre - * Whose primary function would be to reach new resources of type "resource". - */ -m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) -{ - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then look for a good spot. - - Engine.ProfileStart("findEconomicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); - let ccList = []; - for (let cc of ccEnts.values()) - ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); - let dpList = []; - for (let 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; - let nbShips = this.navalManager.transportShips.length; - if (proximity) // this is our first base - { - // if our first base, ensure room around - radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); - // scale is the typical scale at which we want to find a location for our first base - // look for bigger scale if we start from a ship (access < 2) or from a small island - let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; - proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); - if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) - scale = 400 * 400; - } - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). - let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; - let nearbyRejected = Math.square(120); // Reject if too near from any cc - let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc - let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc - let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc - let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc - let 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // We require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - if (proxyAccess && nbShips == 0 && proxyAccess != index) - continue; - - let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps - // Checking distance to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // 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 + API3.SquareVectorDistance(proximity, pos) / scale; - else - { - let minDist = Math.min(); - let accessible = false; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < nearbyRejected) - { - norm = 0; - break; - } - if (!cc.ally) - continue; - if (dist < nearbyAllyRejected) - { - norm = 0; - break; - } - if (dist < nearbyAllyDisfavored) - norm *= 0.5; - - if (dist < minDist) - minDist = dist; - accessible = accessible || index == m.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; - } - - // Not near any of our dropsite, except for oversea docks - oversea = !accessible && dpList.some(dp => m.getLandAccess(gameState, dp.ent) == index); - if (!oversea) - { - for (let dp of dpList) - { - let dist = API3.SquareVectorDistance(dp.pos, pos); - if (dist < 3600) - { - norm = 0; - break; - } - else if (dist < 6400) - norm *= 0.5; - } - } - if (norm == 0) - continue; - } - - if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map - norm *= 0.5; - - let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - val += gameState.sharedScript.ccResourceMaps[res].map[j]; - val *= norm; - - // If 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) - API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); - // not good enough. - if (bestVal < cut) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new Civil Centre - * Whose primary function would be to assure territorial continuity with our allies - */ -m.HQ.prototype.findStrategicCCLocation = function(gameState, template) -{ - // This builds a map. The procedure is fairly simple. - // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC - // with the constraints that all CC have dist > 200 and at least one have dist < 400 - // This needs at least 2 CC. Otherwise, go back to economic CC. - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let ccList = []; - let numAllyCC = 0; - for (let cc of ccEnts.values()) - { - let ally = gameState.isPlayerAlly(cc.owner()); - ccList.push({ "pos": cc.position(), "ally": ally }); - if (ally) - ++numAllyCC; - } - if (numAllyCC < 2) - return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); - - Engine.ProfileStart("findStrategicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestVal; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let currentVal, delta; - let distcc0, distcc1, distcc2; - let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // we require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - - // checking distances to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - let minDist = Math.min(); - distcc0 = undefined; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < 14000) // Reject if too near from any cc - { - minDist = 0; - break; - } - if (!cc.ally) - continue; - if (dist < 62000) // Reject if quite near from ally cc - { - minDist = 0; - break; - } - if (dist < minDist) - minDist = dist; - - if (!distcc0 || dist < distcc0) - { - distcc2 = distcc1; - distcc1 = distcc0; - distcc0 = dist; - } - else if (!distcc1 || dist < distcc1) - { - distcc2 = distcc1; - distcc1 = dist; - } - else if (!distcc2 || dist < distcc2) - distcc2 = dist; - } - if (minDist < 1 || minDist > 170000 && !this.navalMap) - continue; - - delta = Math.sqrt(distcc0) - favoredDistance; - currentVal = delta*delta; - delta = Math.sqrt(distcc1) - favoredDistance; - currentVal += delta*delta; - if (distcc2) - { - delta = Math.sqrt(distcc2) - favoredDistance; - currentVal += delta*delta; - } - // disfavor border of the map - if (this.borderMap.map[j] & m.fullBorder_Mask) - currentVal += 10000; - - if (bestVal !== undefined && currentVal > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = currentVal; - bestIdx = i; - } - - if (this.Config.debug > 1) - API3.warn("We've found a strategic base with bestVal = " + bestVal); - - Engine.ProfileStop(); - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new market: if the allies already have a market, build it as far as possible - * from it, although not in our border to be able to defend it easily. If no allied market, our second market will - * follow the same logic. - * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance - * for performance reasons. - */ -m.HQ.prototype.findMarketLocation = function(gameState, template) -{ - let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); - if (!markets.length) - markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); - - if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let bestDistSq; - let bestGainMult; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - let isNavalMarket = template.hasClass("NavalMarket"); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - // do not try on the narrow border of our territory - if (this.borderMap.map[j] & m.narrowFrontier_Mask) - continue; - if (this.basesMap.map[j] == 0) // only in our territory - continue; - // with enough room around to build the market - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other markets - let maxVal = 0; - let maxDistSq; - let maxGainMult; - let gainMultiplier; - for (let market of markets) - { - if (isNavalMarket && market.hasClass("NavalMarket")) - { - if (m.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) - continue; - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - } - else if (m.getLandAccess(gameState, market) == index && - !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else - continue; - if (!gainMultiplier) - continue; - let distSq = API3.SquareVectorDistance(market.position(), pos); - if (gainMultiplier * distSq > maxVal) - { - maxVal = gainMultiplier * distSq; - maxDistSq = distSq; - maxGainMult = gainMultiplier; - } - } - if (maxVal == 0) - continue; - if (bestVal !== undefined && maxVal < bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = maxVal; - bestDistSq = maxDistSq; - bestGainMult = maxGainMult; - bestIdx = i; - bestJdx = j; - } - - if (this.Config.debug > 1) - API3.warn("We found a market position with bestVal = " + bestVal); - - if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); - if (this.Config.debug > 1) - API3.warn("this would give a trading gain of " + expectedGain); - // do not keep it if gain is too small, except if this is our first BarterMarket - let idx; - if (expectedGain < this.tradeManager.minimalGain) - { - if (template.hasClass("BarterMarket") && - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - idx = -1; // needed by queueplanBuilding manager to keep that market - else - return false; - } - else - idx = this.basesMap.map[bestJdx]; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let 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 - */ -m.HQ.prototype.findDefensiveLocation = function(gameState, template) -{ - // We take the point in our territory which is the nearest to any enemy cc - // but requiring a minimal distance with our other defensive structures - // and not in range of any enemy defensive structure to avoid building under fire. - - let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); - let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals - { - enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) - enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) - return undefined; - } - enemyStructures = enemyStructures.toEntityArray(); - - let wonderMode = gameState.getVictoryConditions().has("wonder"); - let wonderDistmin; - let wonders; - if (wonderMode) - { - wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); - wonderMode = wonders.length != 0; - if (wonderMode) - wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); - } - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let isTower = template.hasClass("Tower"); - let isFortress = template.hasClass("Fortress"); - let radius; - if (isFortress) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (!wonderMode) - { - // do not try if well inside or outside territory - if (!(this.borderMap.map[j] & m.fullFrontier_Mask)) - continue; - if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower) - continue; - } - if (this.basesMap.map[j] == 0) // inaccessible cell - continue; - // with enough room around to build the cc - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other structures - let minDist = Math.min(); - - let dista = 0; - if (wonderMode) - { - dista = API3.SquareVectorDistance(wonders[0].position(), pos); - if (dista < wonderDistmin) - continue; - dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder - } - - for (let str of enemyStructures) - { - if (str.foundationProgress() !== undefined) - continue; - let strPos = str.position(); - if (!strPos) - continue; - let dist = API3.SquareVectorDistance(strPos, pos); - if (dist < 6400) // TODO check on true attack range instead of this 80*80 - { - minDist = -1; - break; - } - if (str.hasClass("CivCentre") && dist + dista < minDist) - minDist = dist + dista; - } - if (minDist < 0) - continue; - - let cutDist = 900; // 30*30 TODO maybe increase it - for (let str of ownStructures) - { - let strPos = str.position(); - if (!strPos) - continue; - if (API3.SquareVectorDistance(strPos, pos) < cutDist) - { - minDist = -1; - break; - } - } - if (minDist < 0 || minDist == Math.min()) - continue; - if (bestVal !== undefined && minDist > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = minDist; - bestIdx = i; - bestJdx = j; - } - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.basesMap.map[bestJdx]]; -}; - -m.HQ.prototype.buildTemple = function(gameState, queues) -{ - // at least one market (which have the same queue) should be build before any temple - if (queues.economicBuilding.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - // Try to build a temple earlier if in regicide to recruit healer guards - if (this.currentPhase < 3 && !gameState.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 m.ConstructionPlan(gameState, templateName)); -}; - -m.HQ.prototype.buildMarket = function(gameState, queues) -{ - if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_market")) - return; - - if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) - { - if (!queues.economicBuilding.paused) - { - // Put available resources in this market - let queueManager = gameState.ai.queueManager; - let cost = queues.economicBuilding.plans[0].getCost(); - queueManager.setAccounts(gameState, cost, "economicBuilding"); - if (!queueManager.canAfford("economicBuilding", cost)) - { - for (let q in queueManager.queues) - { - if (q == "economicBuilding") - continue; - queueManager.transferAccounts(cost, q, "economicBuilding"); - if (queueManager.canAfford("economicBuilding", cost)) - break; - } - } - } - return; - } - - gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -/** Build a farmstead */ -m.HQ.prototype.buildFarmstead = function(gameState, queues) -{ - // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) - if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) - return; - // Wait to have at least one dropsite and house before the farmstead - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) - return; - if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) - return; - if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) - return; - if (!this.canBuild(gameState, "structures/{civ}_farmstead")) - return; - - queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); -}; - -/** - * Try to build a wonder when required - * force = true when called from the victoryManager in case of Wonder victory condition. - */ -m.HQ.prototype.buildWonder = function(gameState, queues, force = false) -{ - if (queues.wonder && queues.wonder.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_wonder")) - return; - - if (!force) - { - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_wonder")); - // Check that we have enough resources to start thinking to build a wonder - let cost = template.cost(); - let resources = gameState.getResources(); - let highLevel = 0; - let lowLevel = 0; - for (let res in cost) - { - if (resources[res] && resources[res] > 0.7 * cost[res]) - ++highLevel; - else if (!resources[res] || resources[res] < 0.3 * cost[res]) - ++lowLevel; - } - if (highLevel == 0 || lowLevel > 1) - return; - } - - queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); -}; - -/** Build a corral, and train animals there */ -m.HQ.prototype.manageCorral = function(gameState, queues) -{ - if (queues.corral.hasQueuedUnits()) - return; - - let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; - if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) && - nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) - { - if (this.canBuild(gameState, "structures/{civ}_corral")) - { - queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); - return; - } - if (!nCorral) - return; - } - - // And train some animals - let civ = gameState.getPlayerCiv(); - for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) - { - if (corral.foundationProgress() !== undefined) - continue; - let trainables = corral.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.isHuntable()) - continue; - let count = gameState.countEntitiesByType(trainable, true); - for (let item of corral.trainingQueue()) - count += item.count; - if (count > nCorral) - continue; - queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); - return; - } - } -}; - -/** - * build more houses if needed. - * kinda ugly, lots of special cases to both build enough houses but not tooo many… - */ -m.HQ.prototype.buildMoreHouses = function(gameState, queues) -{ - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) || - gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return; - - let numPlanned = queues.house.length(); - if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) - { - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); - // change the starting condition according to the situation. - plan.goRequirement = "houseNeeded"; - queues.house.addPlan(plan); - } - - if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) - { - let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); - let houseTemplate = gameState.getTemplate(houseTemplateName); - - let needed = 0; - for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) - { - if (!houseTemplate.hasClass(entityReq.class)) - continue; - - let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; - if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to be less restrictive"); - this.buildManager.setBuildable(houseTemplateName); - this.requireHouses = true; - } - needed = Math.max(needed, entityReq.count - count); - } - - let houseQueue = queues.house.plans; - for (let i = 0; i < numPlanned; ++i) - if (houseQueue[i].isGo(gameState)) - --needed; - else if (needed > 0) - { - houseQueue[i].goRequirement = undefined; - --needed; - } - } - - if (this.requireHouses) - { - let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); - if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => - !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) - this.requireHouses = undefined; - } - - // When population limit too tight - // - if no room to build, try to improve with technology - // - otherwise increase temporarily the priority of houses - let house = gameState.applyCiv("structures/{civ}_house"); - let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; - let popBonus = gameState.getTemplate(house).getPopulationBonus(); - let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); - let priority; - if (freeSlots < 5) - { - if (this.buildManager.isUnbuildable(gameState, house)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - priority = 2*this.Config.priorities.house; - } - else - priority = this.Config.priorities.house; - - if (priority && priority != gameState.ai.queueManager.getPriority("house")) - gameState.ai.queueManager.changePriority("house", priority); -}; - -/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ -m.HQ.prototype.checkBaseExpansion = function(gameState, queues) -{ - if (queues.civilCentre.hasQueuedUnits()) - return; - // First build one cc if all have been destroyed - if (this.numPotentialBases() == 0) - { - this.buildFirstBase(gameState); - return; - } - // Then expand if we have not enough room available for buildings - if (this.buildManager.numberMissingRoom(gameState) > 1) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because not enough room to build "); - this.buildNewBase(gameState, queues); - return; - } - // If we've already planned to phase up, wait a bit before trying to expand - if (this.phasing) - return; - // Finally expand if we have lots of units (threshold depending on the aggressivity value) - let activeBases = this.numActiveBases(); - let numUnits = gameState.getOwnUnits().length; - let numvar = 10 * (1 - this.Config.personality.aggressive); - if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); - this.buildNewBase(gameState, queues); - } -}; - -m.HQ.prototype.buildNewBase = function(gameState, queues, resource) -{ - if (this.numPotentialBases() >= m.maxBaseCount) - return false; - if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) - return false; - if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) - return false; - - let template; - // We require at least one of this civ civCentre as they may allow specific units or techs - let hasOwnCC = false; - for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) - { - if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}_civil_centre")) - continue; - hasOwnCC = true; - break; - } - if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else if (this.canBuild(gameState, "structures/{civ}_civil_centre")) - template = "structures/{civ}_civil_centre"; - else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else - return false; - - // base "-1" means new base. - if (this.Config.debug > 1) - API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); - queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); - return true; -}; - -/** Deals with building fortresses and towers along our border with enemies. */ -m.HQ.prototype.buildDefenses = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) - return; - - if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) - { - // try to build fortresses - if (this.canBuild(gameState, "structures/{civ}_fortress")) - { - let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; - if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && - numFortresses < this.numActiveBases() + 1 + this.extraFortresses && - numFortresses < Math.floor(gameState.getPopulation() / 25) && - gameState.getOwnFoundationsByClass("Fortress").length < 2) - { - this.fortressStartTime = gameState.ai.elapsedTime; - if (!numFortresses) - gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - return; - } - } - } - - if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) - { - let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers - let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime; - if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) - { - this.fortStartTime = gameState.ai.elapsedTime; - queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); - } - return; - } - - if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) - return; - - let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; - let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; - if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) && - numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && - numTowers < Math.floor(gameState.getPopulation() / 8) && - gameState.getOwnFoundationsByClass("DefenseTower").length < 3) - { - this.towerStartTime = gameState.ai.elapsedTime; - if (numTowers > 2 * this.numActiveBases() + 3) - gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - } -}; - -m.HQ.prototype.buildBlacksmith = function(gameState, queues) -{ - if (this.getAccountedPopulation(gameState) < this.Config.Military.popForBlacksmith || - queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) - return; - // build a market before the blacksmith - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - - if (this.canBuild(gameState, "structures/{civ}_blacksmith")) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); -}; - -/** - * Deals with constructing military buildings (barracks, stables…) - * They are mostly defined by Config.js. This is unreliable since changes could be done easily. - */ -m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) - return; - - let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; - if (this.saveResources && numBarracks != 0) - return; - - let barracksTemplate = this.canBuild(gameState, "structures/{civ}_barracks") ? "structures/{civ}_barracks" : undefined; - - let rangeTemplate = this.canBuild(gameState, "structures/{civ}_range") ? "structures/{civ}_range" : undefined; - let numRanges = gameState.getOwnEntitiesByClass("Archery", true).length; - numBarracks -= numRanges; - - let stableTemplate = this.canBuild(gameState, "structures/{civ}_stables") ? "structures/{civ}_stables" : - this.canBuild(gameState, "structures/{civ}_stable") ? "structures/{civ}_stable" : undefined; - let numStables = gameState.getOwnEntitiesByClass("Stables", true).length; - - if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || - this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) - { - // first barracks/range and stables. - if (numBarracks + numRanges == 0) - { - let template = barracksTemplate || rangeTemplate; - if (template) - { - gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); - let plan = new m.ConstructionPlan(gameState, template, { "militaryBase": true }); - plan.queueToReset = "militaryBuilding"; - queues.militaryBuilding.addPlan(plan); - return; - } - } - if (numStables == 0 && stableTemplate) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Second range/barracks and stables - if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Then 3rd barracks/range/stables if needed - if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) - { - let template = barracksTemplate || stableTemplate || rangeTemplate; - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - } - - if (this.saveResources) - return; - - if (this.currentPhase < 3) - return; - - if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true })); - return; - } - - if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true })); - return; - } - - if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) - return; - - // Build advanced military buildings - let nAdvanced = 0; - for (let advanced of this.bAdvanced) - nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); - - if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) - { - for (let advanced of this.bAdvanced) - { - if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) - continue; - let template = gameState.getTemplate(advanced); - if (!template) - continue; - let civ = gameState.getPlayerCiv(); - if (template.hasDefensiveFire() || template.trainableEntities(civ)) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true })); - else // not a military building, but still use this queue - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); - return; - } - } -}; - -/** - * Find base nearest to ennemies for military buildings. - */ -m.HQ.prototype.findBestBaseForMilitary = function(gameState) -{ - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); - let bestBase; - let enemyFound = false; - let distMin = Math.min(); - for (let cce of ccEnts) - { - if (gameState.isPlayerAlly(cce.owner())) - continue; - if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) - continue; - let access = m.getLandAccess(gameState, cce); - let isEnemy = gameState.isPlayerEnemy(cce.owner()); - for (let cc of ccEnts) - { - if (cc.owner() != PlayerID) - continue; - if (m.getLandAccess(gameState, cc) != access) - continue; - let dist = API3.SquareVectorDistance(cc.position(), cce.position()); - if (!enemyFound && isEnemy) - enemyFound = true; - else if (dist > distMin) - continue; - bestBase = cc.getMetadata(PlayerID, "base"); - distMin = dist; - } - } - return bestBase; -}; - -/** - * train with highest priority ranged infantry in the nearest civil centre from a given set of positions - * and garrison them there for defense - */ -m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) -{ - if (gameState.ai.queues.emergency.hasQueuedUnits()) - return false; - - let civ = gameState.getPlayerCiv(); - // find nearest base anchor - let distcut = 20000; - let nearestAnchor; - let distmin; - for (let pos of positions) - { - let access = gameState.ai.accessibility.getAccessValue(pos); - // check nearest base anchor - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - if (!base.anchor.trainableEntities(civ)) // base still in construction - continue; - let queue = base.anchor._entity.trainingQueue; - if (queue) - { - let time = 0; - for (let item of queue) - if (item.progress > 0 || item.metadata && item.metadata.garrisonType) - time += item.timeRemaining; - if (time/1000 > 5) - continue; - } - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (nearestAnchor && dist > distmin) - continue; - distmin = dist; - nearestAnchor = base.anchor; - } - } - if (!nearestAnchor || distmin > distcut) - return false; - - // We will choose randomly ranged and melee units, except when garrisonHolder is full - // in which case we prefer melee units - let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); - if (nearestAnchor._entity.trainingQueue) - { - for (let item of nearestAnchor._entity.trainingQueue) - { - if (item.metadata && item.metadata.garrisonType) - numGarrisoned += item.count; - else if (!item.progress && (!item.metadata || !item.metadata.trainer)) - nearestAnchor.stopProduction(item.id); - } - } - let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && - nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); - let rangedWanted = randBool() && autogarrison; - - let total = gameState.getResources(); - let templateFound; - let trainables = nearestAnchor.trainableEntities(civ); - let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) - continue; - if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) - continue; - if (!total.canAfford(new API3.Resources(template.cost()))) - continue; - templateFound = [trainable, template]; - if (template.hasClass("Ranged") == rangedWanted) - break; - } - if (!templateFound) - return false; - - // Check first if we can afford it without touching the other accounts - // and if not, take some of other accounted resources - // TODO sort the queues to be substracted - let queueManager = gameState.ai.queueManager; - let cost = new API3.Resources(templateFound[1].cost()); - queueManager.setAccounts(gameState, cost, "emergency"); - if (!queueManager.canAfford("emergency", cost)) - { - for (let q in queueManager.queues) - { - if (q == "emergency") - continue; - queueManager.transferAccounts(cost, q, "emergency"); - if (queueManager.canAfford("emergency", cost)) - break; - } - } - let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; - if (autogarrison) - metadata.garrisonType = "protection"; - gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); - return true; -}; - -m.HQ.prototype.canBuild = function(gameState, structure) -{ - let type = gameState.applyCiv(structure); - if (this.buildManager.isUnbuildable(gameState, type)) - return false; - - if (gameState.isTemplateDisabled(type)) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); - return false; - } - - let template = gameState.getTemplate(type); - if (!template) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); - return false; - } - - if (!template.available(gameState)) - { - this.buildManager.setUnbuildable(gameState, type, 30, "tech"); - return false; - } - - if (!this.buildManager.hasBuilder(type)) - { - this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); - return false; - } - - if (this.numActiveBases() < 1) - { - // if no base, check that we can build outside our territory - let buildTerritories = template.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) - { - this.buildManager.setUnbuildable(gameState, type, 180, "room"); - return false; - } - } - - // build limits - let limits = gameState.getEntityLimits(); - let category = template.buildCategory(); - if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) - { - this.buildManager.setUnbuildable(gameState, type, 90, "limit"); - return false; - } - - return true; -}; - -m.HQ.prototype.updateTerritories = function(gameState) -{ - const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; - let alliedVictory = gameState.getAlliedVictory(); - let passabilityMap = gameState.getPassabilityMap(); - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let insideSmall = Math.round(45 / cellSize); - let insideLarge = Math.round(80 / cellSize); // should be about the range of towers - let expansion = 0; - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.borderMap.map[j] & m.outside_Mask) - continue; - if (this.borderMap.map[j] & m.fullFrontier_Mask) - this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier - - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - // If this tile was already accounted, remove it - if (this.basesMap.map[j] == 0) - continue; - let base = this.getBaseByID(this.basesMap.map[j]); - if (base) - { - let index = base.territoryIndices.indexOf(j); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); - } - else - API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); - this.basesMap.map[j] = 0; - } - else - { - // Update the frontier - let ix = j%width; - let iz = Math.floor(j/width); - let onFrontier = false; - for (let a of around) - { - let jx = ix + Math.round(insideSmall*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(insideSmall*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - { - this.borderMap.map[j] |= m.narrowFrontier_Mask; - break; - } - jx = ix + Math.round(insideLarge*a[0]); - if (jx < 0 || jx >= width) - continue; - jz = iz + Math.round(insideLarge*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - onFrontier = true; - } - if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask)) - this.borderMap.map[j] |= m.largeFrontier_Mask; - - // If this tile was not already accounted, add it. - if (this.basesMap.map[j] != 0) - continue; - let landPassable = false; - let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); - let access; - for (let k of ind) - { - if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - continue; - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - continue; - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - expansion++; - } - } - - if (!expansion) - return; - // We've increased our territory, so we may have some new room to build - this.buildManager.resetMissingRoom(gameState); - // And if sufficient expansion, check if building a new market would improve our present trade routes - let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; - if (expansion * cellArea > 960) - this.tradeManager.routeProspection = true; -}; - -/** Reassign territories when a base is going to be deleted */ -m.HQ.prototype.reassignTerritories = function(deletedBase) -{ - let cellSize = this.territoryMap.cellSize; - let width = this.territoryMap.width; - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != deletedBase.accessIndex) - continue; - let dist = API3.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; - } -}; - -/** - * returns the base corresponding to baseID - */ -m.HQ.prototype.getBaseByID = function(baseID) -{ - for (let base of this.baseManagers) - if (base.ID == baseID) - return base; - - return undefined; -}; - -/** - * returns the number of bases with a cc - * ActiveBases includes only those with a built cc - * PotentialBases includes also those with a cc in construction - */ -m.HQ.prototype.numActiveBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; -}; - -m.HQ.prototype.numPotentialBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -m.HQ.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (let base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } -}; - -m.HQ.prototype.resetBaseCache = function() -{ - this.turnCache.base = undefined; -}; - -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). - */ -m.HQ.prototype.assignGatherers = function() -{ - for (let base of this.baseManagers) - { - for (let worker of base.workers.values()) - { - if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE") - continue; - let orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); - } - } -}; - -m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) -{ - return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); -}; - -/** Check that the chosen position is not too near from an invading army */ -m.HQ.prototype.isNearInvadingArmy = function(pos) -{ - for (let army of this.defenseManager.armies) - if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) - return true; - return false; -}; - -m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) -{ - if (!this.turnCache.firingStructures) - this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); - for (let ent of this.turnCache.firingStructures.values()) - { - let range = radius + ent.attackRange("Ranged").max; - if (API3.SquareVectorDistance(ent.position(), pos) < range*range) - return true; - } - return false; -}; - -/** Compute the capture strength of all units attacking a capturable target */ -m.HQ.prototype.updateCaptureStrength = function(gameState) -{ - this.capturableTargets.clear(); - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.canCapture()) - continue; - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || !target.isCapturable() || !ent.canCapture(target)) - continue; - if (!this.capturableTargets.has(targetId)) - this.capturableTargets.set(targetId, { - "strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"), - "ents": new Set([ent.id()]) - }); - else - { - let capturableTarget = this.capturableTargets.get(target.id()); - capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTarget.ents.add(ent.id()); - } - } - - for (let [targetId, capturableTarget] of this.capturableTargets) - { - let target = gameState.getEntityById(targetId); - let allowCapture; - for (let entId of capturableTarget.ents) - { - let ent = gameState.getEntityById(entId); - if (allowCapture === undefined) - allowCapture = m.allowCapture(gameState, ent, target); - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].attackType) - continue; - if ((orderData[0].attackType == "Capture") !== allowCapture) - ent.attack(targetId, allowCapture); - } - } - - this.capturableTargetsTime = gameState.ai.elapsedTime; -}; - -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** add a gatherer to the turn cache for this supply. */ -m.HQ.prototype.AddTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) - ++this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = 1; - } -}; - -/** remove a gatherer to the turn cache for this supply. */ -m.HQ.prototype.RemoveTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - --this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = -1; - } -}; - -m.HQ.prototype.GetTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - return this.turnCache.resourceGatherer[supplyID]; - - return 0; -}; - -/** The next two are to register that we assigned a gatherer to a resource this turn. */ -m.HQ.prototype.AddTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - ++this.turnCache["resourceGatherer-" + resource]; - else - this.turnCache["resourceGatherer-" + resource] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -m.HQ.prototype.GetTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - return this.turnCache["resourceGatherer-" + resource]; - - return 0; -}; - -/** - * flag a resource as exhausted - */ -m.HQ.prototype.isResourceExhausted = function(resource) -{ - if (this.turnCache["exhausted-" + resource] == undefined) - this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); - - return this.turnCache["exhausted-" + resource]; -}; - -/** - * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) - */ -m.HQ.prototype.isDefendable = function(ent) -{ - if (!this.turnCache.numAround) - this.turnCache.numAround = {}; - if (this.turnCache.numAround[ent.id()] === undefined) - this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); - return +this.turnCache.numAround[ent.id()] > 8; -}; - -/** - * Get the number of population already accounted for - */ -m.HQ.prototype.getAccountedPopulation = function(gameState) -{ - if (this.turnCache.accountedPopulation == undefined) - { - let pop = gameState.getPopulation(); - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.unitTemplate) - continue; - let 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 - */ -m.HQ.prototype.getAccountedWorkers = function(gameState) -{ - if (this.turnCache.accountedWorkers == undefined) - { - let workers = gameState.getOwnEntitiesByRole("worker", true).length; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") - continue; - workers += item.count; - } - } - this.turnCache.accountedWorkers = workers; - } - return this.turnCache.accountedWorkers; -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.HQ.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Headquarters update"); - this.turnCache = {}; - this.territoryMap = m.createTerritoryMap(gameState); - this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities(); - // TODO find a better way to update - if (this.currentPhase != gameState.currentPhase()) - { - if (this.Config.debug > 0) - API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + - " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + - " phasing " + this.phasing); - this.currentPhase = gameState.currentPhase(); - - // In principle, this.phasing should be already reset to 0 when starting the research - // but this does not work in case of an autoResearch tech - if (this.phasing) - this.phasing = 0; - } - -/* if (this.Config.debug > 1) - { - gameState.getOwnUnits().forEach (function (ent) { - if (!ent.position()) - return; - m.dumpEntity(ent); - }); - } */ - - this.checkEvents(gameState, events); - this.navalManager.checkEvents(gameState, queues, events); - - if (this.phasing) - this.checkPhaseRequirements(gameState, queues); - else - this.researchManager.checkPhase(gameState, queues); - - if (this.numActiveBases() > 0) - { - if (gameState.ai.playedTurn % 4 == 0) - this.trainMoreWorkers(gameState, queues); - - if (gameState.ai.playedTurn % 4 == 1) - this.buildMoreHouses(gameState, queues); - - if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) - this.buildFarmstead(gameState, queues); - - if (this.needCorral && gameState.ai.playedTurn % 4 == 3) - this.manageCorral(gameState, queues); - - if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) - this.researchManager.update(gameState, queues); - } - - if (this.numPotentialBases() < 1 || - this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) - this.checkBaseExpansion(gameState, queues); - - if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) - { - if (!this.canBarter) - this.buildMarket(gameState, queues); - - if (!this.saveResources) - { - this.buildBlacksmith(gameState, queues); - this.buildTemple(gameState, queues); - } - - if (gameState.ai.playedTurn % 30 == 0 && - gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) - this.buildWonder(gameState, queues, false); - } - - this.tradeManager.update(gameState, events, queues); - - this.garrisonManager.update(gameState, events); - this.defenseManager.update(gameState, events); - - if (gameState.ai.playedTurn % 3 == 0) - { - this.constructTrainingBuildings(gameState, queues); - if (this.Config.difficulty > 0) - this.buildDefenses(gameState, queues); - } - - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase; // We will loop only on 1 active base per turn - do - { - 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]); - } - while (!activeBase && nbBases != 0); - - this.navalManager.update(gameState, queues, events); - - if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) - this.attackManager.update(gameState, queues, events); - - this.diplomacyManager.update(gameState, events); - - this.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(); -}; - -m.HQ.prototype.Serialize = function() -{ - let properties = { - "phasing": this.phasing, - "currentBase": this.currentBase, - "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 - }; - - let baseManagers = []; - for (let base of this.baseManagers) - baseManagers.push(base.Serialize()); - - if (this.Config.debug == -100) - { - API3.warn(" HQ serialization ---------------------"); - API3.warn(" properties " + uneval(properties)); - API3.warn(" baseManagers " + uneval(baseManagers)); - API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); - API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); - API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); - API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); - API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); - API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); - API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); - API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); - API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); - } - - return { - "properties": properties, - - "baseManagers": baseManagers, - "attackManager": this.attackManager.Serialize(), - "buildManager": this.buildManager.Serialize(), - "defenseManager": this.defenseManager.Serialize(), - "tradeManager": this.tradeManager.Serialize(), - "navalManager": this.navalManager.Serialize(), - "researchManager": this.researchManager.Serialize(), - "diplomacyManager": this.diplomacyManager.Serialize(), - "garrisonManager": this.garrisonManager.Serialize(), - "victoryManager": this.victoryManager.Serialize(), - }; -}; - -m.HQ.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.baseManagers = []; - for (let base of data.baseManagers) - { - // the first call to deserialize set the ID base needed by entitycollections - let newbase = new m.BaseManager(gameState, this.Config); - newbase.Deserialize(gameState, base); - newbase.init(gameState); - newbase.Deserialize(gameState, base); - this.baseManagers.push(newbase); - } - - this.navalManager = new m.NavalManager(this.Config); - this.navalManager.init(gameState, true); - this.navalManager.Deserialize(gameState, data.navalManager); - - this.attackManager = new m.AttackManager(this.Config); - this.attackManager.Deserialize(gameState, data.attackManager); - this.attackManager.init(gameState); - this.attackManager.Deserialize(gameState, data.attackManager); - - this.buildManager = new m.BuildManager(); - this.buildManager.Deserialize(data.buildManager); - - this.defenseManager = new m.DefenseManager(this.Config); - this.defenseManager.Deserialize(gameState, data.defenseManager); - - this.tradeManager = new m.TradeManager(this.Config); - this.tradeManager.init(gameState); - this.tradeManager.Deserialize(gameState, data.tradeManager); - - this.researchManager = new m.ResearchManager(this.Config); - this.researchManager.Deserialize(data.researchManager); - - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.diplomacyManager.Deserialize(data.diplomacyManager); - - this.garrisonManager = new m.GarrisonManager(this.Config); - this.garrisonManager.Deserialize(data.garrisonManager); - - this.victoryManager = new m.VictoryManager(this.Config); - this.victoryManager.Deserialize(data.victoryManager); -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-patriot/mapModule.js b/install/petraBased/petra-patriot/mapModule.js deleted file mode 100644 index 225767a..0000000 --- a/install/petraBased/petra-patriot/mapModule.js +++ /dev/null @@ -1,221 +0,0 @@ -var PETRA = function(m) -{ - -/** map functions */ - -m.TERRITORY_PLAYER_MASK = 0x1F; -m.TERRITORY_BLINKING_MASK = 0x40; - -m.createObstructionMap = function(gameState, accessIndex, template) -{ - let passabilityMap = gameState.getPassabilityMap(); - let territoryMap = gameState.ai.territoryMap; - let ratio = territoryMap.cellSize / passabilityMap.cellSize; - - // default values - let placementType = "land"; - let buildOwn = true; - let buildAlly = true; - let buildNeutral = true; - let buildEnemy = false; - // If there is a template then replace the defaults - if (template) - { - placementType = template.buildPlacementType(); - buildOwn = template.hasBuildTerritory("own"); - buildAlly = template.hasBuildTerritory("ally"); - buildNeutral = template.hasBuildTerritory("neutral"); - buildEnemy = template.hasBuildTerritory("enemy"); - } - let obstructionTiles = new Uint8Array(passabilityMap.data.length); - - let passMap; - let obstructionMask; - if (placementType == "shore") - { - passMap = gameState.ai.accessibility.navalPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-shore"); - } - else - { - passMap = gameState.ai.accessibility.landPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-land"); - } - - for (let k = 0; k < territoryMap.data.length; ++k) - { - let tilePlayer = territoryMap.data[k] & m.TERRITORY_PLAYER_MASK; - let isConnected = (territoryMap.data[k] & m.TERRITORY_BLINKING_MASK) == 0; - if (tilePlayer === PlayerID) - { - if (!buildOwn || !buildNeutral && !isConnected) - continue; - } - else if (gameState.isPlayerMutualAlly(tilePlayer)) - { - if (!buildAlly || !buildNeutral && !isConnected) - continue; - } - else if (tilePlayer === 0) - { - if (!buildNeutral) - continue; - } - else - { - if (!buildEnemy) - continue; - } - - let x = ratio * (k % territoryMap.width); - let y = ratio * Math.floor(k / territoryMap.width); - for (let ix = 0; ix < ratio; ++ix) - { - for (let iy = 0; iy < ratio; ++iy) - { - let i = x + ix + (y + iy)*passabilityMap.width; - if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) - continue; - if (!(passabilityMap.data[i] & obstructionMask)) - obstructionTiles[i] = 255; - } - } - } - - let map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); - map.setMaxVal(255); - - if (template && template.buildDistance()) - { - let distance = template.buildDistance(); - let minDist = distance.MinDistance ? +distance.MinDistance : 0; - if (minDist) - { - let obstructionRadius = template.obstructionRadius(); - if (obstructionRadius) - minDist -= obstructionRadius.min; - let fromClass = distance.FromClass; - let cellSize = passabilityMap.cellSize; - let cellDist = 1 + minDist / cellSize; - let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); - for (let ent of structures.values()) - { - if (!ent.position()) - continue; - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - map.addInfluence(x, z, cellDist, -255, "constant"); - } - } - } - - return map; -}; - - -m.createTerritoryMap = function(gameState) -{ - let map = gameState.ai.territoryMap; - - let ret = new API3.Map(gameState.sharedScript, "territory", map.data); - ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; - ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; - ret.isBlinking = function(p) { return (this.point(p) & m.TERRITORY_BLINKING_MASK) != 0; }; - return ret; -}; - -/** - * The borderMap contains some border and frontier information: - * - border of the map filled once: - * - all mini-cells (1x1) from the big cell (8x8) inaccessibles => bit 0 - * - inside a given distance to the border => bit 1 - * - frontier of our territory (updated regularly in updateFrontierMap) - * - narrow border (inside our territory) => bit 2 - * - large border (inside our territory, exclusive of narrow) => bit 3 - */ - -m.outside_Mask = 1; -m.border_Mask = 2; -m.fullBorder_Mask = m.outside_Mask | m.border_Mask; -m.narrowFrontier_Mask = 4; -m.largeFrontier_Mask = 8; -m.fullFrontier_Mask = m.narrowFrontier_Mask | m.largeFrontier_Mask; - -m.createBorderMap = function(gameState) -{ - let map = new API3.Map(gameState.sharedScript, "territory"); - let width = map.width; - let border = Math.round(80 / map.cellSize); - let passabilityMap = gameState.getPassabilityMap(); - let obstructionMask = gameState.getPassabilityClassMask("unrestricted"); - if (gameState.circularMap) - { - let ic = (width - 1) / 2; - let radcut = (ic - border) * (ic - border); - for (let j = 0; j < map.length; ++j) - { - let dx = j%width - ic; - let dy = Math.floor(j/width) - ic; - let radius = dx*dx + dy*dy; - if (radius < radcut) - continue; - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - else - { - let borderCut = width - border; - for (let j = 0; j < map.length; ++j) - { - let ix = j%width; - let iy = Math.floor(j/width); - if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) - { - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - } - -// map.dumpIm("border.png", 5); - return map; -}; - -m.debugMap = function(gameState, map) -{ - let width = map.width; - let cell = map.cellSize; - gameState.getEntities().forEach(ent => { - let pos = ent.position(); - if (!pos) - return; - let x = Math.round(pos[0] / cell); - let z = Math.round(pos[1] / cell); - let id = x + width*z; - if (map.map[id] == 1) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else if (map.map[id] == 2) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - else if (map.map[id] == 3) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 0, 2] }); - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/navalManager.js b/install/petraBased/petra-patriot/navalManager.js deleted file mode 100644 index 5a16d0f..0000000 --- a/install/petraBased/petra-patriot/navalManager.js +++ /dev/null @@ -1,896 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Naval Manager - * Will deal with anything ships. - * -Basically trade over water (with fleets and goals commissioned by the economy manager) - * -Defense over water (commissioned by the defense manager) - * -Transport of units over water (a few units). - * -Scouting, ultimately. - * Also deals with handling docks, making sure we have access and stuffs like that. - */ - -m.NavalManager = function(Config) -{ - this.Config = Config; - - // ship subCollections. Also exist for land zones, idem, not caring. - this.seaShips = []; - this.seaTransportShips = []; - this.seaWarShips = []; - this.seaFishShips = []; - - // wanted NB per zone. - this.wantedTransportShips = []; - this.wantedWarShips = []; - this.wantedFishShips = []; - // needed NB per zone. - this.neededTransportShips = []; - this.neededWarShips = []; - - this.transportPlans = []; - - // shore-line regions where we can load and unload units - this.landingZones = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.NavalManager.prototype.init = function(gameState, deserializing) -{ - // docks - this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"])); - this.docks.registerUpdates(); - - this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); - // note: those two can overlap (some transport ships are warships too and vice-versa). - this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); - this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); - this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); - - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - this.warShips.registerUpdates(); - this.fishShips.registerUpdates(); - - let availableFishes = {}; - for (let fish of gameState.getFishableSupplies().values()) - { - let sea = this.getFishSea(gameState, fish); - if (sea && availableFishes[sea]) - availableFishes[sea] += fish.resourceSupplyAmount(); - else if (sea) - availableFishes[sea] = fish.resourceSupplyAmount(); - } - - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (!gameState.ai.HQ.navalRegions[i]) - { - // push dummies - this.seaShips.push(undefined); - this.seaTransportShips.push(undefined); - this.seaWarShips.push(undefined); - this.seaFishShips.push(undefined); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - else - { - let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaShips.push(collec); - collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaTransportShips.push(collec); - collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaWarShips.push(collec); - collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaFishShips.push(collec); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - if (availableFishes[i] && availableFishes[i] > 1000) - this.wantedFishShips.push(this.Config.Economy.targetNumFishers); - else - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - } - - if (deserializing) - return; - - // determination of the possible landing zones - let width = gameState.getPassabilityMap().width; - let length = width * gameState.getPassabilityMap().height; - for (let i = 0; i < length; ++i) - { - let land = gameState.ai.accessibility.landPassMap[i]; - if (land < 2) - continue; - let naval = gameState.ai.accessibility.navalPassMap[i]; - if (naval < 2) - continue; - if (!this.landingZones[land]) - this.landingZones[land] = {}; - if (!this.landingZones[land][naval]) - this.landingZones[land][naval] = new Set(); - this.landingZones[land][naval].add(i); - } - // and keep only thoses with enough room around when possible - for (let land in this.landingZones) - { - for (let sea in this.landingZones[land]) - { - let landing = this.landingZones[land][sea]; - let nbaround = {}; - let nbcut = 0; - for (let i of landing) - { - let nb = 0; - if (landing.has(i-1)) - nb++; - if (landing.has(i+1)) - nb++; - if (landing.has(i+width)) - nb++; - if (landing.has(i-width)) - nb++; - nbaround[i] = nb; - nbcut = Math.max(nb, nbcut); - } - nbcut = Math.min(2, nbcut); - for (let i of landing) - { - if (nbaround[i] < nbcut) - landing.delete(i); - } - } - } - - // Assign our initial docks and ships - for (let ship of this.ships.values()) - m.setSeaAccess(gameState, ship); - for (let dock of this.docks.values()) - m.setSeaAccess(gameState, dock); -}; - -m.NavalManager.prototype.updateFishingBoats = function(sea, num) -{ - if (this.wantedFishShips[sea]) - this.wantedFishShips[sea] = num; -}; - -m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) -{ - if (sea !== undefined) - this.wantedFishShips[sea] = 0; - else - this.wantedFishShips.fill(0); -}; - -/** Get the sea, cache it if not yet done and check if in opensea */ -m.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]]; - let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); - let width = gameState.ai.accessibility.width; - let k = pos[0] + pos[1]*width; - sea = gameState.ai.accessibility.navalPassMap[k]; - fish.setMetadata(PlayerID, "sea", sea); - let radius = 120 / gameState.ai.accessibility.cellSize / ntry; - if (around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let 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) - { - let 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 */ -m.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]]; - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let radius = 120 / territoryMap.cellSize / ntry; - let pos = territoryMap.gamePosToMapPos(fish.position()); - return around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); - if (i < 0 || i >= width || j < 0 || j >= width) - continue; - let 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 */ -m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) -{ - let seas = gameState.ai.accessibility.regionLinks[region].slice(); - this.docks.forEach(dock => { - if (!dock.hasClass("Dock") || m.getLandAccess(gameState, dock) != region) - return; - let i = seas.indexOf(m.getSeaAccess(gameState, dock)); - if (i != -1) - seas.splice(i--, 1); - }); - return seas; -}; - -m.NavalManager.prototype.checkEvents = function(gameState, queues, events) -{ - for (let evt of events.Create) - { - if (!evt.entity) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } - - for (let evt of events.TrainingFinished) - { - if (!evt.entities) - continue; - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) - continue; - m.setSeaAccess(gameState, ent); - } - } - - for (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) - continue; - if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) - continue; - let plan = this.getPlan(evt.metadata[PlayerID].transporter); - if (!plan) - continue; - - let shipId = evt.entityObj.id(); - if (this.Config.debug > 1) - API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); - if (plan.state == "boarding") - { - // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan - plan.units.forEach(ent => { - if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() || - ent.getMetadata(PlayerID, "onBoard") == shipId) - ent.setMetadata(PlayerID, "onBoard", undefined); - }); - plan.needTransportShips = !plan.transportShips.hasEntities(); - } - else if (plan.state == "sailing") - { - let endIndex = plan.endIndex; - for (let ent of plan.units.values()) - { - if (!ent.position()) // unit from another ship of this plan ... do nothing - continue; - let access = m.getLandAccess(gameState, ent); - let endPos = ent.getMetadata(PlayerID, "endPos"); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // nothing else to do if access = endIndex as already at destination - // otherwise, we should require another transport - // TODO if attacking and no more ships available, remove the units from the attack - // to avoid delaying it too much - if (access != endIndex) - this.requireTransport(gameState, ent, access, endIndex, endPos); - } - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.to !== PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } -}; - - -m.NavalManager.prototype.getPlan = function(ID) -{ - for (let plan of this.transportPlans) - if (plan.ID === ID) - return plan; - return undefined; -}; - -m.NavalManager.prototype.addPlan = function(plan) -{ - this.transportPlans.push(plan); -}; - -/** - * complete already existing plan or create a new one for this requirement - * (many units can then call this separately and end up in the same plan) - * TODO check garrison classes - */ -m.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) -{ - if (!ent.canGarrison()) - return false; - - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - if (this.Config.debug > 0) - API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport"); - return false; - } - - let plans = []; - for (let plan of this.transportPlans) - { - if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding") - continue; - // Limit the number of siege units per transport to avoid problems when ungarrisoning - if (m.isSiegeUnit(ent) && plan.units.filter(unit => m.isSiegeUnit(unit)).length > 3) - continue; - plans.push(plan); - } - - if (plans.length) - { - plans.sort(plan => plan.units.length); - plans[0].addUnit(ent, endPos); - return true; - } - - let plan = new m.TransportPlan(gameState, [ent], startIndex, endIndex, endPos); - if (plan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> transport plan aborted <<<<"); - return false; - } - plan.init(gameState); - this.transportPlans.push(plan); - return true; -}; - -/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ -m.NavalManager.prototype.splitTransport = function(gameState, plan) -{ - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan started <<<<"); - let newplan = new m.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); - if (newplan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan aborted <<<<"); - return false; - } - newplan.init(gameState); - - for (let 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 - */ -m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) -{ - let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); - if (fromAccess !== 1) - return; - if (toAccess < 2) - return; - - for (let ship of this.ships.values()) - { - if (!ship.isGarrisonHolder() || !ship.garrisoned().length) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined) - continue; - let units = []; - for (let entId of ship.garrisoned()) - units.push(gameState.getEntityById(entId)); - // TODO check that the garrisoned units have not another purpose - let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); - if (plan.failed) - continue; - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - -// set minimal number of needed ships when a new event (new base or new attack plan) -m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) -{ - if (!sea) - return; - if (this.wantedTransportShips[sea] < number) - this.wantedTransportShips[sea] = number; -}; - -// bumps up the number of ships we want if we need more. -m.NavalManager.prototype.checkLevels = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - this.neededTransportShips[sea] = 0; - - for (let plan of this.transportPlans) - { - if (!plan.needTransportShips || plan.units.length < 2) - continue; - let sea = plan.sea; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || - this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - continue; - ++this.neededTransportShips[sea]; - if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) - { - ++this.wantedTransportShips[sea]; - return; - } - } - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - if (this.neededTransportShips[sea] > 2) - ++this.wantedTransportShips[sea]; -}; - -m.NavalManager.prototype.maintainFleet = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) - return; - // check if we have enough transport ships per region. - for (let sea = 0; sea < this.seaShips.length; ++sea) - { - if (this.seaShips[sea] === undefined) - continue; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) - continue; - - if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - { - let template = this.getBestShip(gameState, sea, "transport"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); - continue; - } - } - - - if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) - { - let template = this.getBestShip(gameState, sea, "fishing"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); - continue; - } - } - } -}; - -/** assigns free ships to plans that need some */ -m.NavalManager.prototype.assignShipsToPlans = function(gameState) -{ - for (let plan of this.transportPlans) - if (plan.needTransportShips) - plan.assignShip(gameState); -}; - -/** Return true if this ship is likeky (un)garrisoning units */ -m.NavalManager.prototype.isShipBoarding = function(ship) -{ - if (!ship.position()) - return false; - let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); - if (!plan || !plan.boardingPos[ship.id()]) - return false; - return API3.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. - */ -m.NavalManager.prototype.moveApart = function(gameState) -{ - let blockedShips = []; - let blockedIds = []; - - for (let ship of this.ships.values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) - continue; - - let unitAIState = ship.unitAIState(); - if (ship.getMetadata(PlayerID, "transporter") !== undefined || - unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - - } - } - - for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - let role = ship.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - } - } - - for (let ship of blockedShips) - { - let shipPosition = ship.position(); - let sea = ship.getMetadata(PlayerID, "sea"); - for (let blockingShip of this.seaShips[sea].values()) - { - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && - unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && - unitAIState != "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - - for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - if (blockingShip.getMetadata(PlayerID, "sea") != sea) - continue; - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let role = blockingShip.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - } -}; - -m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) -{ - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) - return; - - if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) - { - if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && - !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock")) - { - let dockStarted = false; - for (let base of gameState.ai.HQ.baseManagers) - { - if (dockStarted) - break; - if (!base.anchor || base.constructing) - continue; - let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); - for (let sea of remaining) - { - if (!gameState.ai.HQ.navalRegions[sea]) - continue; - let wantedLand = {}; - wantedLand[base.accessIndex] = true; - queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); - dockStarted = true; - break; - } - } - } - } - - if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || - queues.militaryBuilding.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || - this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) - return; - // Use in priority resources to build a market - if (!gameState.getOwnEntitiesByClass("BarterMarket", 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; - let wantedLand = {}; - for (let base of gameState.ai.HQ.baseManagers) - if (base.anchor) - wantedLand[base.accessIndex] = true; - let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); -}; - -/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ -m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) -{ - let civ = gameState.getPlayerCiv(); - let trainableShips = []; - gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { - let trainables = ent.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) - trainableShips.push(trainable); - } - }); - - let best = 0; - let bestShip; - let limits = gameState.getEntityLimits(); - let current = gameState.getEntityCounts(); - for (let trainable of trainableShips) - { - let template = gameState.getTemplate(trainable); - if (!template.available(gameState)) - continue; - - let category = template.trainingCategory(); - if (category && limits[category] && current[category] >= limits[category]) - continue; - - let arrows = +(template.getDefaultArrow() || 0); - if (goal === "attack") // choose the maximum default arrows - { - if (best > arrows) - continue; - best = arrows; - } - else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport - { - let capacity = +(template.garrisonMax() || 0); - if (capacity < 2) - continue; - capacity += 10*arrows; - if (MatchesClassList(template.garrisonableClasses(), "Siege")) - capacity += 50; - if (best > capacity) - continue; - best = capacity; - } - else if (goal === "fishing") - if (!template.hasClass("FishingBoat")) - continue; - bestShip = trainable; - } - return bestShip; -}; - -m.NavalManager.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Naval Manager update"); - - // close previous transport plans if finished - for (let i = 0; i < this.transportPlans.length; ++i) - { - let remaining = this.transportPlans[i].update(gameState); - if (remaining) - continue; - if (this.Config.debug > 1) - API3.warn("no more units on transport plan " + this.transportPlans[i].ID); - this.transportPlans[i].releaseAll(); - this.transportPlans.splice(i--, 1); - } - // assign free ships to plans which need them - this.assignShipsToPlans(gameState); - - // and require for more ships/structures if needed - if (gameState.ai.playedTurn % 3 === 0) - { - this.checkLevels(gameState, queues); - this.maintainFleet(gameState, queues); - this.buildNavalStructures(gameState, queues); - } - // let inactive ships move apart from active ones (waiting for a better pathfinder) - this.moveApart(gameState); - - Engine.ProfileStop(); -}; - -m.NavalManager.prototype.Serialize = function() -{ - let properties = { - "wantedTransportShips": this.wantedTransportShips, - "wantedWarShips": this.wantedWarShips, - "wantedFishShips": this.wantedFishShips, - "neededTransportShips": this.neededTransportShips, - "neededWarShips": this.neededWarShips, - "landingZones": this.landingZones - }; - - let transports = {}; - for (let plan in this.transportPlans) - transports[plan] = this.transportPlans[plan].Serialize(); - - return { "properties": properties, "transports": transports }; -}; - -m.NavalManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.transportPlans = []; - for (let i in data.transports) - { - let dataPlan = data.transports[i]; - let plan = new m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); - plan.Deserialize(dataPlan); - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queue.js b/install/petraBased/petra-patriot/queue.js deleted file mode 100644 index b11b2e1..0000000 --- a/install/petraBased/petra-patriot/queue.js +++ /dev/null @@ -1,167 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Holds a list of wanted plans to train or construct - */ - -m.Queue = function() -{ - this.plans = []; - this.paused = false; - this.switched = 0; -}; - -m.Queue.prototype.empty = function() -{ - this.plans = []; -}; - -m.Queue.prototype.addPlan = function(newPlan) -{ - if (!newPlan) - return; - for (let 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); -}; - -m.Queue.prototype.check= function(gameState) -{ - while (this.plans.length > 0) - { - if (!this.plans[0].isInvalid(gameState)) - return; - let plan = this.plans.shift(); - if (plan.queueToReset) - gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); - } -}; - -m.Queue.prototype.getNext = function() -{ - if (this.plans.length > 0) - return this.plans[0]; - return null; -}; - -m.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 - */ -m.Queue.prototype.maxAccountWanted = function(gameState, fraction) -{ - let cost = new API3.Resources(); - 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) - { - let costs = this.plans[1].getCost(); - costs.multiply(fraction); - cost.add(costs); - } - return cost; -}; - -m.Queue.prototype.queueCost = function() -{ - let cost = new API3.Resources(); - for (let plan of this.plans) - cost.add(plan.getCost()); - return cost; -}; - -m.Queue.prototype.length = function() -{ - return this.plans.length; -}; - -m.Queue.prototype.hasQueuedUnits = function() -{ - return this.plans.length > 0; -}; - -m.Queue.prototype.countQueuedUnits = function() -{ - let count = 0; - for (let plan of this.plans) - count += plan.number; - return count; -}; - -m.Queue.prototype.hasQueuedUnitsWithClass = function(classe) -{ - return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); -}; - -m.Queue.prototype.countQueuedUnitsWithClass = function(classe) -{ - let count = 0; - for (let plan of this.plans) - if (plan.template && plan.template.hasClass(classe)) - count += plan.number; - return count; -}; - -m.Queue.prototype.countQueuedUnitsWithMetadata = function(data, value) -{ - let count = 0; - for (let plan of this.plans) - if (plan.metadata[data] && plan.metadata[data] == value) - count += plan.number; - return count; -}; - -m.Queue.prototype.Serialize = function() -{ - let plans = []; - for (let plan of this.plans) - plans.push(plan.Serialize()); - - return { "plans": plans, "paused": this.paused, "switched": this.switched }; -}; - -m.Queue.prototype.Deserialize = function(gameState, data) -{ - this.paused = data.paused; - this.switched = data.switched; - this.plans = []; - for (let dataPlan of data.plans) - { - let plan; - if (dataPlan.category == "unit") - plan = new m.TrainingPlan(gameState, dataPlan.type); - else if (dataPlan.category == "building") - plan = new m.ConstructionPlan(gameState, dataPlan.type); - else if (dataPlan.category == "technology") - plan = new m.ResearchPlan(gameState, dataPlan.type); - else - { - API3.warn("Petra deserialization error: plan unknown " + uneval(dataPlan)); - continue; - } - plan.Deserialize(gameState, dataPlan); - this.plans.push(plan); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queueManager.js b/install/petraBased/petra-patriot/queueManager.js deleted file mode 100644 index fa74b8d..0000000 --- a/install/petraBased/petra-patriot/queueManager.js +++ /dev/null @@ -1,542 +0,0 @@ -var PETRA = function(m) -{ - -// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. -// -// Currently this manager keeps accounts for each queue, split between the 4 main resources -// -// Each time resources are available (ie not in any account), it is split between the different queues -// Mostly based on priority of the queue, and existing needs. -// Each turn, the queue Manager checks if a queue can afford its next item, then it does. -// -// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it -// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will -// be able to benefit form the 500 food (even if they only needed food). -// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. -// -// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues -// get some part of the total, and if all queues have 70% of their needs, nothing gets done -// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. -// -// This system should be improved. It's probably not flexible enough. - -m.QueueManager = function(Config, queues) -{ - this.Config = Config; - this.queues = queues; - this.priorities = {}; - for (let i in Config.priorities) - this.priorities[i] = Config.priorities[i]; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in this.queues) - { - this.accounts[q] = new API3.Resources(); - this.queueArrays.push([q, this.queues[q]]); - } - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getAvailableResources = function(gameState) -{ - let resources = gameState.getResources(); - for (let key in this.queues) - resources.subtract(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.getTotalAccountedResources = function() -{ - let resources = new API3.Resources(); - for (let key in this.queues) - resources.add(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.currentNeeds = function(gameState) -{ - let needed = new API3.Resources(); - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) - continue; - let costs = queue.plans[0].getCost(); - needed.add(costs); - } - // get out current resources, not removing accounts. - let current = gameState.getResources(); - for (let res of Resources.GetCodes()) - needed[res] = Math.max(0, needed[res] - current[res]); - - return needed; -}; - -// calculate the gather rates we'd want to be able to start all elements in our queues -// TODO: many things. -m.QueueManager.prototype.wantedGatherRates = function(gameState) -{ - // default values for first turn when we have not yet set our queues. - if (gameState.ai.playedTurn === 0) - { - let ret = {}; - for (let res of Resources.GetCodes()) - ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; - return ret; - } - - // get out current resources, not removing accounts. - let current = gameState.getResources(); - // short queue is the first item of a queue, assumed to be ready in 30s - // medium queue is the second item of a queue, assumed to be ready in 60s - // long queue contains the isGo=false items, assumed to be ready in 300s - let totalShort = {}; - let totalMedium = {}; - let totalLong = {}; - for (let res of Resources.GetCodes()) - { - totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; - totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; - totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; - } - let total; - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (queue.paused) - continue; - for (let j = 0; j < queue.length(); ++j) - { - if (j > 1) - break; - let cost = queue.plans[j].getCost(); - if (queue.plans[j].isGo(gameState)) - { - if (j === 0) - total = totalShort; - else - total = totalMedium; - } - else - total = totalLong; - for (let type in total) - total[type] += cost[type]; - if (!queue.plans[j].isGo(gameState)) - break; - } - } - // global rates - let rates = {}; - let diff; - for (let res of Resources.GetCodes()) - { - if (current[res] > 0) - { - diff = Math.min(current[res], totalShort[res]); - totalShort[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - { - diff = Math.min(current[res], totalMedium[res]); - totalMedium[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - totalLong[res] -= Math.min(current[res], totalLong[res]); - } - } - rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; - } - - return rates; -}; - -m.QueueManager.prototype.printQueues = function(gameState) -{ - let numWorkers = 0; - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - numWorkers++; - }); - API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); - for (let i in this.queues) - { - let q = this.queues[i]; - if (q.hasQueuedUnits()) - { - API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); - API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); - } - for (let plan of q.plans) - { - let qStr = " " + plan.type + " "; - if (plan.number) - qStr += "x" + plan.number; - qStr += " isGo " + plan.isGo(gameState); - API3.warn(qStr); - } - } - API3.warn("Accounts"); - for (let p in this.accounts) - API3.warn(p + ": " + uneval(this.accounts[p])); - API3.warn("Current Resources: " + uneval(gameState.getResources())); - API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); - API3.warn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); - API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); - API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); - API3.warn("------------------------------------"); -}; - -m.QueueManager.prototype.clear = function() -{ - for (let i in this.queues) - this.queues[i].empty(); -}; - -/** - * set accounts of queue i from the unaccounted resources - */ -m.QueueManager.prototype.setAccounts = function(gameState, cost, i) -{ - let available = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (this.accounts[i][res] >= cost[res]) - continue; - this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); - } -}; - -/** - * transfer accounts from queue i to queue j - */ -m.QueueManager.prototype.transferAccounts = function(cost, i, j) -{ - for (let res of Resources.GetCodes()) - { - if (this.accounts[j][res] >= cost[res]) - continue; - let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); - this.accounts[i][res] -= diff; - this.accounts[j][res] += diff; - } -}; - -/** - * distribute the resources between the different queues according to their priorities - */ -m.QueueManager.prototype.distributeResources = function(gameState) -{ - let availableRes = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) - { - let total = gameState.getResources()[res]; - let scale = total / (total - availableRes[res]); - availableRes[res] = total; - for (let j in this.queues) - { - this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); - availableRes[res] -= this.accounts[j][res]; - } - } - - if (!availableRes[res]) - { - this.switchResource(gameState, res); - continue; - } - - for (let q in this.queues) { - let queueCost = this.queues[q].maxAccountWanted(gameState, 0); - if (this.queues[q].hasQueuedUnits() && this.accounts[q][res] < queueCost[res] && !this.queues[q].paused) { - this.accounts[q][res] = queueCost[res]; - } - } - } -}; - -m.QueueManager.prototype.switchResource = function(gameState, res) -{ - // We have no available resources, see if we can't "compact" them in one queue. - // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. - // TODO: this isn't perfect compression. - for (let j in this.queues) - { - if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) - continue; - - let queue = this.queues[j]; - let queueCost = queue.maxAccountWanted(gameState, 0); - if (this.accounts[j][res] >= queueCost[res]) - continue; - - for (let i in this.queues) - { - if (i === j) - continue; - let otherQueue = this.queues[i]; - if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) - continue; - if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) - continue; - - let diff = queueCost[res] - this.accounts[j][res]; - this.accounts[j][res] += diff; - this.accounts[i][res] -= diff; - ++otherQueue.switched; - if (this.Config.debug > 2) - API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); - break; - } - } -}; - -// Start the next item in the queue if we can afford it. -m.QueueManager.prototype.startNextItems = function(gameState) -{ - for (let q of this.queueArrays) - { - let name = q[0]; - let queue = q[1]; - if (queue.hasQueuedUnits() && !queue.paused) - { - let item = queue.getNext(); - if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) - { - // canStart may update the cost because of the costMultiplier so we must check it again - if (this.accounts[name].canAfford(item.getCost())) - { - this.finishingTime = gameState.ai.elapsedTime; - this.accounts[name].subtract(item.getCost()); - queue.startNext(gameState); - queue.switched = 0; - } - } - } - else if (!queue.hasQueuedUnits()) - { - this.accounts[name].reset(); - queue.switched = 0; - } - } -}; - -m.QueueManager.prototype.update = function(gameState) -{ - Engine.ProfileStart("Queue Manager"); - - for (let i in this.queues) - { - this.queues[i].check(gameState); // do basic sanity checks on the queue - if (this.priorities[i] > 0) - continue; - API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); - this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. - } - - // Pause or unpause queues depending on the situation - this.checkPausedQueues(gameState); - - // Let's assign resources to plans that need them - this.distributeResources(gameState); - - // Start the next item in the queue if we can afford it. - this.startNextItems(gameState); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - this.printQueues(gameState); - - Engine.ProfileStop(); -}; - -// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training -m.QueueManager.prototype.checkPausedQueues = function(gameState) -{ - let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); - let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); - for (let q in this.queues) - { - let toBePaused = false; - if (gameState.ai.HQ.numPotentialBases() == 0) - toBePaused = q != "dock" && q != "civilCentre"; - else if (numWorkers < workersMin / 3) - toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; - else if (numWorkers < workersMin * 2 / 3) - toBePaused = q == "civilCentre" || q == "economicBuilding" || - q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || - q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; - else if (numWorkers < workersMin) - toBePaused = q == "civilCentre" || q == "defenseBuilding" || - q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; - - if (toBePaused) - { - if (q == "field" && gameState.ai.HQ.needFarm && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "corral" && gameState.ai.HQ.needCorral && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "dock" && gameState.ai.HQ.needFish && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) - toBePaused = false; - if (q == "ships" && gameState.ai.HQ.needFish && - !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) - toBePaused = false; - } - - let queue = this.queues[q]; - if (!queue.paused && toBePaused) - { - queue.paused = true; - this.accounts[q].reset(); - } - else if (queue.paused && !toBePaused) - queue.paused = false; - - // And reduce the batch sizes of attack queues - if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) - { - queue.plans[0].number = 1; - if (queue.plans[1]) - queue.plans[1].number = 1; - } - } -}; - -m.QueueManager.prototype.canAfford = function(queue, cost) -{ - if (!this.accounts[queue]) - return false; - return this.accounts[queue].canAfford(cost); -}; - -m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) -{ - if (!this.queues[queue]) - return; - this.queues[queue].paused = true; - if (scrapAccounts) - this.accounts[queue].reset(); -}; - -m.QueueManager.prototype.unpauseQueue = function(queue) -{ - if (this.queues[queue]) - this.queues[queue].paused = false; -}; - -m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) -{ - for (let q in this.queues) - { - if (q == but) - continue; - if (scrapAccounts) - this.accounts[q].reset(); - this.queues[q].paused = true; - } -}; - -m.QueueManager.prototype.unpauseAll = function(but) -{ - for (let q in this.queues) - if (q != but) - this.queues[q].paused = false; -}; - - -m.QueueManager.prototype.addQueue = function(queueName, priority) -{ - if (this.queues[queueName] !== undefined) - return; - - this.queues[queueName] = new m.Queue(); - this.priorities[queueName] = priority; - this.accounts[queueName] = new API3.Resources(); - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.removeQueue = function(queueName) -{ - if (this.queues[queueName] === undefined) - return; - - delete this.queues[queueName]; - delete this.priorities[queueName]; - delete this.accounts[queueName]; - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getPriority = function(queueName) -{ - return this.priorities[queueName]; -}; - -m.QueueManager.prototype.changePriority = function(queueName, newPriority) -{ - if (this.Config.debug > 1) - API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); - if (this.queues[queueName] !== undefined) - this.priorities[queueName] = newPriority; - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.Serialize = function() -{ - let accounts = {}; - let queues = {}; - for (let q in this.queues) - { - queues[q] = this.queues[q].Serialize(); - accounts[q] = this.accounts[q].Serialize(); - if (this.Config.debug == -100) - API3.warn("queueManager serialization: queue " + q + " >>> " + - uneval(queues[q]) + " with accounts " + uneval(accounts[q])); - } - - return { - "priorities": this.priorities, - "queues": queues, - "accounts": accounts - }; -}; - -m.QueueManager.prototype.Deserialize = function(gameState, data) -{ - this.priorities = data.priorities; - this.queues = {}; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in data.queues) - { - this.queues[q] = new m.Queue(); - this.queues[q].Deserialize(gameState, data.queues[q]); - this.accounts[q] = new API3.Resources(); - this.accounts[q].Deserialize(data.accounts[q]); - this.queueArrays.push([q, this.queues[q]]); - } - this.queueArrays.sort((a, b) => data.priorities[b[0]] - data.priorities[a[0]]); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queueplan.js b/install/petraBased/petra-patriot/queueplan.js deleted file mode 100644 index ba4ebf5..0000000 --- a/install/petraBased/petra-patriot/queueplan.js +++ /dev/null @@ -1,70 +0,0 @@ -var PETRA = function(m) -{ -/** - * Common functions and variables to all queue plans. - */ - -m.QueuePlan = function(gameState, type, metadata) -{ - this.type = gameState.applyCiv(type); - this.metadata = metadata; - - this.template = gameState.getTemplate(this.type); - if (!this.template) - { - API3.warn("Tried to add the inexisting template " + this.type + " to Petra."); - return false; - } - this.ID = gameState.ai.uniqueIDs.plans++; - this.cost = new API3.Resources(this.template.cost()); - this.number = 1; - this.category = ""; - - return true; -}; - -/** Check the content of this queue */ -m.QueuePlan.prototype.isInvalid = function(gameState) -{ - return false; -}; - -/** if true, the queue manager will begin increasing this plan's account. */ -m.QueuePlan.prototype.isGo = function(gameState) -{ - return true; -}; - -/** can we start this plan immediately? */ -m.QueuePlan.prototype.canStart = function(gameState) -{ - return false; -}; - -/** process the plan. */ -m.QueuePlan.prototype.start = function(gameState) -{ - // should call onStart. -}; - -m.QueuePlan.prototype.getCost = function() -{ - let costs = new API3.Resources(); - costs.add(this.cost); - if (this.number !== 1) - costs.multiply(this.number); - return costs; -}; - -/** - * On Event functions. - * Can be used to do some specific stuffs - * Need to be updated to actually do something if you want them to. - * this is called by "Start" if it succeeds. - */ -m.QueuePlan.prototype.onStart = function(gameState) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queueplanBuilding.js b/install/petraBased/petra-patriot/queueplanBuilding.js deleted file mode 100644 index 841d5d2..0000000 --- a/install/petraBased/petra-patriot/queueplanBuilding.js +++ /dev/null @@ -1,951 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Defines a construction plan, ie a building. - * We'll try to fing a good position if non has been provided - */ - -m.ConstructionPlan = function(gameState, type, metadata, position) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - return false; - - this.position = position ? position : 0; - - this.category = "building"; - - return true; -}; - -m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ConstructionPlan.prototype.canStart = function(gameState) -{ - if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn - return false; - - if (!this.isGo(gameState)) - return false; - - if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) - return false; - - return gameState.ai.HQ.buildManager.hasBuilder(this.type); -}; - -m.ConstructionPlan.prototype.start = function(gameState) -{ - Engine.ProfileStart("Building construction start"); - - // We don't care which builder we assign, since they won't actually do - // the building themselves - all we care about is that there is at least - // one unit that can start the foundation (should always be the case here). - let builder = gameState.findBuilder(this.type); - if (!builder) - { - API3.warn("petra error: builder not found when starting construction."); - Engine.ProfileStop(); - return; - } - - let pos = this.findGoodPosition(gameState); - if (!pos) - { - gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); - Engine.ProfileStop(); - return; - } - - if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("BarterMarket") || - gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())) - { - // Check if this market is still worth building (others may have been built making it useless) - let tradeManager = gameState.ai.HQ.tradeManager; - tradeManager.checkRoutes(gameState); - if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) - { - Engine.ProfileStop(); - return; - } - } - gameState.ai.HQ.turnCache.buildingBuilt = true; - - if (this.metadata === undefined) - this.metadata = { "base": pos.base }; - else if (this.metadata.base === undefined) - this.metadata.base = pos.base; - - if (pos.access) - this.metadata.access = pos.access; // needed for Docks whose position is on water - else - this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); - - if (this.template.buildPlacementType() == "shore") - { - // adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) - builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); - else // try with the lowest, move towards us unless we're same - { - for (let step = 0; step <= 1; step += 0.2) - builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, - pos.angle, this.metadata); - } - this.onStart(gameState); - Engine.ProfileStop(); - - if (this.metadata && this.metadata.proximity) - gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); -}; - -m.ConstructionPlan.prototype.findGoodPosition = function(gameState) -{ - let template = this.template; - - if (template.buildPlacementType() == "shore") - return this.findDockPosition(gameState); - - let HQ = gameState.ai.HQ; - if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) - { - // recompute the best dropsite location in case some conditions have changed - let base = HQ.getBaseByID(this.metadata.base); - let type = this.metadata.type ? this.metadata.type : "wood"; - let newpos = base.findBestDropsiteLocation(gameState, type); - if (newpos && newpos.quality > 0) - { - let pos = newpos.pos; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; - } - } - - if (!this.position) - { - if (template.hasClass("CivCentre")) - { - let pos; - if (this.metadata && this.metadata.resource) - { - let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; - pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); - } - else - pos = HQ.findStrategicCCLocation(gameState, template); - - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; - // No possible location, try to build instead a dock in a not-enemy island - let templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) - { - template = gameState.getTemplate(templateName); - if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) - this.buildOverseaDock(gameState, template); - } - return false; - } - else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) - { - let pos = HQ.findDefensiveLocation(gameState, template); - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - // if this fortress is our first one, just try the standard placement - if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) - return false; - } - else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before - { - let pos = HQ.findMarketLocation(gameState, template); - if (pos && pos[2] > 0) - { - if (!this.metadata) - this.metadata = {}; - this.metadata.expectedGain = pos[3]; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - } - else if (!pos) - return false; - } - } - - // Compute each tile's closeness to friendly structures: - - let placement = new API3.Map(gameState.sharedScript, "territory"); - let cellSize = placement.cellSize; // size of each tile - - let alreadyHasHouses = false; - - if (this.position) // If a position was specified then place the building as close to it as possible - { - let x = Math.floor(this.position[0] / cellSize); - let z = Math.floor(this.position[1] / cellSize); - placement.addInfluence(x, z, 255); - } - else // No position was specified so try and find a sensible place to build - { - // give a small > 0 level as the result of addInfluence is constrained to be > 0 - // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] == base) - placement.set(j, 45); - } - else - { - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] != 0) - placement.set(j, 45); - } - - if (!HQ.requireHouses || !template.hasClass("House")) - { - gameState.getOwnStructures().forEach(function(ent) { - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - - let struct = m.getBuiltEntity(gameState, ent); - if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) - { - if (template.hasClass("Field") || template.hasClass("Corral")) - placement.addInfluence(x, z, 80/cellSize, 50); - else // If this is not a field add a negative influence because we want to leave this area for fields - placement.addInfluence(x, z, 80/cellSize, -20); - } - else if (template.hasClass("House")) - { - if (ent.hasClass("House")) - { - placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses - alreadyHasHouses = true; - } - else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) - placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs - } - else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && - (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) - placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) - else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) - placement.addInfluence(x, z, 120/cellSize, -50); - else if (template.hasClass("Military")) - placement.addInfluence(x, z, 40/cellSize, -40); - else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) - placement.addInfluence(x, z, 60/cellSize, 40); - }); - } - if (template.hasClass("Farmstead")) - { - for (let j = 0; j < placement.map.length; ++j) - { - let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; - if (HQ.borderMap.map[j] & m.fullBorder_Mask) - value /= 2; // we need space around farmstead, so disfavor map border - placement.set(j, value); - } - } - } - - // Requires to be inside our territory, and inside our base territory if required - // and if our first market, put it on border if possible to maximize distance with next market - let favorBorder = template.hasClass("BarterMarket"); - let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); - let favoredBase = this.metadata && (this.metadata.favoredBase || - (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] != base) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - } - } - } - else - { - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] == 0) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - else if (favoredBase && HQ.basesMap.map[j] == favoredBase) - placement.set(j, placement.map[j] + 100); - } - } - } - - // Find the best non-obstructed: - // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, - // this allows room for units to walk between buildings. - // note: not for houses and dropsites who ought to be closer to either each other or a resource. - // also not for fields who can be stacked quite a bit - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let radius = 0; - if (template.hasClass("Fortress") || template.hasClass("Workshop") || - this.type == gameState.applyCiv("structures/{civ}_elephant_stables")) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && - !template.hasClass("Field") && !template.hasClass("BarterMarket")) - radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let bestTile; - if (template.hasClass("House") && !alreadyHasHouses) - { - // try to get some space to place several houses first - bestTile = placement.findBestTile(3*radius, obstructions); - if (!bestTile.val) - bestTile = undefined; - } - - if (!bestTile) - bestTile = placement.findBestTile(radius, obstructions); - - if (!bestTile.val) - return false; - - let bestIdx = bestTile.idx; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - let territorypos = placement.gamePosToMapPos([x, z]); - let territoryIndex = territorypos[0] + territorypos[1]*placement.width; - // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; -}; - -/** - * Placement of buildings with Dock build category - * metadata.proximity is defined when first dock without any territory - * => we try to minimize distance from our current point - * metadata.oversea is defined for dock in oversea islands - * => we try to maximize distance to our current docks (for trade) - * otherwise standard dock on an island where we already have a cc - * => we try not to be too far from our territory - * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. - */ -m.ConstructionPlan.prototype.findDockPosition = function(gameState) -{ - let template = this.template; - let territoryMap = gameState.ai.HQ.territoryMap; - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let bestIdx; - let bestJdx; - let bestAngle; - let bestLand; - let bestWater; - let bestVal = -1; - let navalPassMap = gameState.ai.accessibility.navalPassMap; - - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - - let nbShips = gameState.ai.HQ.navalManager.transportShips.length; - let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; - let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; - let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; - let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; - if (nbShips == 0 && proxyAccess && proxyAccess > 1) - { - wantedLand = {}; - wantedLand[proxyAccess] = true; - } - let dropsiteTypes = template.resourceDropsiteTypes(); - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let halfSize = 0; // used for dock angle - let halfDepth = 0; // used by checkPlacement - let halfWidth = 0; // used by checkPlacement - if (template.get("Footprint/Square")) - { - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - halfDepth = +template.get("Footprint/Square/@depth") / 2; - halfWidth = +template.get("Footprint/Square/@width") / 2; - } - else if (template.get("Footprint/Circle")) - { - halfSize = +template.get("Footprint/Circle/@radius"); - halfDepth = halfSize; - halfWidth = halfSize; - } - - // res is a measure of the amount of resources around, and maxRes is the max value taken into account - // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement - const maxRes = 10; - const maxWater = 16; - let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; - let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; - // Normalisation factors (only guessed, no attempt to optimize them) - let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; - for (let j = 0; j < territoryMap.length; ++j) - { - if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) - continue; - let score = 0; - if (!proxyAccess && !oversea) - { - // if not in our (or allied) territory, we do not want it too far to be able to defend it - score = this.getFrontierProximity(gameState, j); - if (score > 4) - continue; - score *= factor; - } - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - if (wantedSea && navalPassMap[i] != wantedSea) - continue; - - let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - // If proximity is given, we look for the nearest point - if (proxyAccess) - score = API3.VectorDistance(this.metadata.proximity, pos); - - // Bonus for resources - score += 20 * (maxRes - res); - - if (oversea) - { - // Not much farther to one of our cc than to enemy ones - let enemyDist; - let ownDist; - for (let cc of ccEnts.values()) - { - let owner = cc.owner(); - if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) - continue; - let dist = API3.SquareVectorDistance(pos, cc.position()); - if (owner == PlayerID && (!ownDist || dist < ownDist)) - ownDist = dist; - if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) - enemyDist = dist; - } - if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) - continue; - - // And maximize distance for trade. - let dockDist = 0; - for (let dock of docks.values()) - { - if (m.getSeaAccess(gameState, dock) != navalPassMap[i]) - continue; - let dist = API3.SquareVectorDistance(pos, dock.position()); - if (dist > dockDist) - dockDist = dist; - } - if (dockDist > 0) - { - dockDist = Math.sqrt(dockDist); - if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away - continue; - score += factor * (width * cellSize - dockDist); - } - } - - // Add a penalty if on the map border as ship movement will be difficult - if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) - score += 20; - - // Do a pre-selection, supposing we will have the best possible water - if (bestIdx !== undefined && score > bestVal + 5 * maxWater) - continue; - - let x = (i % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; - let angle = this.getDockAngle(gameState, x, z, halfSize); - if (angle == false) - continue; - let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); - if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) - continue; - // Final selection now that the checkDockPlacement water is known - if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) - continue; - if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - - bestVal = score + maxWater - ret.water; - bestIdx = i; - bestJdx = j; - bestAngle = angle; - bestLand = ret.land; - bestWater = ret.water; - } - if (bestVal < 0) - return false; - - // if no good place with enough water around and still in first phase, wait for expansion at the next phase - if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Assign this dock to a base - let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; - if (!baseIndex) - baseIndex = -2; // We'll do an anchorless base around it - - return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; -}; - -/** - * Find a good island to build a dock. - */ -m.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) -{ - let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); - if (!docks.hasEntities()) - return; - - let passabilityMap = gameState.getPassabilityMap(); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - - let land = {}; - let found; - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (gameState.ai.accessibility.regionType[i] != "land" || - cellArea * gameState.ai.accessibility.regionSize[i] < 3600) - continue; - let keep = true; - for (let dock of docks.values()) - { - if (m.getLandAccess(gameState, dock) != i) - continue; - keep = false; - break; - } - if (!keep) - continue; - let sea; - for (let cc of ccEnts.values()) - { - let ccAccess = m.getLandAccess(gameState, cc); - if (ccAccess != i) - { - if (cc.owner() == PlayerID && !sea) - sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); - continue; - } - // Docks on island where we have a cc are already done elsewhere - if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) - { - keep = false; - break; - } - } - if (!keep || !sea) - continue; - land[i] = true; - found = true; - } - if (!found) - return; - if (!gameState.ai.HQ.navalMap) - API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); - - let oldTemplate = this.template; - let oldMetadata = this.metadata; - this.template = template; - let pos; - this.metadata = { "land": land, "oversea": true }; - pos = this.findDockPosition(gameState); - if (pos) - { - let type = template.templateName(); - let builder = gameState.findBuilder(type); - this.metadata.base = pos.base; - // Adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - this.template = oldTemplate; - this.metadata = oldMetadata; -}; - -/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ -m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) -{ - let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*gameState.ai.accessibility.width; - let seaRef = gameState.ai.accessibility.navalPassMap[k]; - if (seaRef < 2) - return false; - const numPoints = 16; - for (let dist = 0; dist < 4; ++dist) - { - let waterPoints = []; - for (let i = 0; i < numPoints; ++i) - { - let angle = 2 * Math.PI * i / numPoints; - pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; - pos = gameState.ai.accessibility.gamePosToMapPos(pos); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - continue; - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.navalPassMap[j] == seaRef) - waterPoints.push(i); - } - let length = waterPoints.length; - if (!length) - continue; - let consec = []; - for (let i = 0; i < length; ++i) - { - let count = 0; - for (let j = 0; j < length-1; ++j) - { - if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) - ++count; - else - break; - } - consec[i] = count; - } - let start = 0; - let count = 0; - for (let c in consec) - { - if (consec[c] > count) - { - start = c; - count = consec[c]; - } - } - - // If we've found a shoreline, stop searching - if (count != numPoints-1) - return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; - } - return false; -}; - -/** - * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js - * to determine the special dock requirements - * returns {"land": land index for this dock, "water": amount of water around this spot} - */ -m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) -{ - let sz = halfDepth * Math.sin(angle); - let cz = halfDepth * Math.cos(angle); - // center back position - let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[j]; - if (land < 2) - return null; - // center front position - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - return null; - // additional constraints compared to BuildRestriction.js to assure we have enough place to build - let sw = halfWidth * Math.cos(angle) * 3 / 4; - let cw = halfWidth * Math.sin(angle) * 3 / 4; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - let water = 0; - let sp = 15 * Math.sin(angle); - let cp = 15 * Math.cos(angle); - for (let i = 1; i < 5; ++i) - { - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - water += 4; - } - return { "land": land, "water": water }; -}; - -/** - * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension - * if the (object) wantedLand is given, this nearest land should have one of these accessibility - * if wantedSea is given, this tile should be inside this sea - */ -const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], - [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50]]; - -m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) -{ - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - let dimLand = dimension + 1.5 * cellSize; - let dimSea = dimension + 2 * cellSize; - - let accessibility = gameState.ai.accessibility; - let x = (j%width + 0.5) * cellSize; - let z = (Math.floor(j/width) + 0.5) * cellSize; - let pos = accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*accessibility.width; - let landPass = accessibility.landPassMap[k]; - if (landPass > 1 && wantedLand && !wantedLand[landPass] || - landPass < 2 && accessibility.navalPassMap[k] < 2) - return false; - - for (let a of around) - { - pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - landPass = accessibility.landPassMap[k]; - if (landPass < 2 || wantedLand && !wantedLand[landPass]) - continue; - pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - if (wantedSea && accessibility.navalPassMap[k] != wantedSea || - !wantedSea && accessibility.navalPassMap[k] < 2) - continue; - return true; - } - - return false; -}; - -/** - * return a measure of the proximity to our frontier (including our allies) - * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m - */ -m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) -{ - let alliedVictory = gameState.getAlliedVictory(); - let territoryMap = gameState.ai.HQ.territoryMap; - let territoryOwner = territoryMap.getOwnerIndex(j); - if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) - return 0; - - let borderMap = gameState.ai.HQ.borderMap; - let width = territoryMap.width; - let step = Math.round(24 / territoryMap.cellSize); - let ix = j % width; - let iz = Math.floor(j / width); - let best = 5; - for (let a of around) - { - for (let i = 1; i < 5; ++i) - { - let jx = ix + Math.round(i*step*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(i*step*a[1]); - if (jz < 0 || jz >= width) - continue; - if (borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); - if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) - { - best = Math.min(best, i); - break; - } - } - if (best == 1) - break; - } - - return best; -}; - -/** - * get the sum of the resources (except food) around, inside a given radius - * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood - */ -m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) -{ - let resourceMaps = gameState.sharedScript.resourceMaps; - let w = resourceMaps.wood.width; - let cellSize = resourceMaps.wood.cellSize; - let size = Math.floor(radius / cellSize); - let ix = i % w; - let iy = Math.floor(i / w); - let total = 0; - let nbcell = 0; - for (let k of types) - { - if (k == "food" || !resourceMaps[k]) - continue; - let weigh0 = k == "wood" ? 2 : 1; - for (let dy = 0; dy <= size; ++dy) - { - let dxmax = size - dy; - let ky = iy + dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - if (dy == 0) - continue; - ky = iy - dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - } - } - return nbcell ? total / nbcell : 0; -}; - -m.ConstructionPlan.prototype.isGo = function(gameState) -{ - if (this.goRequirement && this.goRequirement == "houseNeeded") - { - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) - return false; - if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return false; - let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); - for (let ent of gameState.getOwnFoundations().values()) - { - let template = gameState.getBuiltTemplate(ent.templateName()); - if (template) - freeSlots += template.getPopulationBonus(); - } - - if (gameState.ai.HQ.saveResources) - return freeSlots <= 10; - if (gameState.getPopulation() > 55) - return freeSlots <= 21; - if (gameState.getPopulation() > 30) - return freeSlots <= 15; - return freeSlots <= 10; - } - return true; -}; - -m.ConstructionPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); -}; - -m.ConstructionPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "position": this.position, - "goRequirement": this.goRequirement || undefined, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ConstructionPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queueplanResearch.js b/install/petraBased/petra-patriot/queueplanResearch.js deleted file mode 100644 index 2e09783..0000000 --- a/install/petraBased/petra-patriot/queueplanResearch.js +++ /dev/null @@ -1,113 +0,0 @@ -var PETRA = function(m) -{ - -m.ResearchPlan = function(gameState, type, rush = false) -{ - if (!m.QueuePlan.call(this, gameState, type, {})) - return false; - - if (this.template.researchTime === undefined) - return false; - - // Refine the estimated cost - let researchers = this.getBestResearchers(gameState, true); - if (researchers) - this.cost = new API3.Resources(this.template.cost(researchers[0])); - - this.category = "technology"; - this.rush = rush; - - return true; -}; - -m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ResearchPlan.prototype.canStart = function(gameState) -{ - this.researchers = this.getBestResearchers(gameState); - if (!this.researchers) - return false; - this.cost = new API3.Resources(this.template.cost(this.researchers[0])); - return true; -}; - -m.ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false) -{ - let allResearchers = gameState.findResearchers(this.type, noRequirementCheck); - if (!allResearchers || !allResearchers.hasEntities()) - return undefined; - - // Keep only researchers with smallest cost - let costMin = Math.min(); - let researchers; - for (let ent of allResearchers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - researchers.push(ent); - else if (cost < costMin) - { - costMin = cost; - researchers = [ent]; - } - } - return researchers; -}; - -m.ResearchPlan.prototype.isInvalid = function(gameState) -{ - return gameState.isResearched(this.type) || gameState.isResearching(this.type); -}; - -m.ResearchPlan.prototype.start = function(gameState) -{ - // Prefer researcher with shortest queues (no need to serialize this.researchers - // as the functions canStart and start are always called on the same turn) - this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); - // Drop anything in the queue if we rush it. - if (this.rush) - this.researchers[0].stopAllProduction(0.45); - this.researchers[0].research(this.type); - this.onStart(gameState); -}; - -m.ResearchPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); - - for (let i = gameState.getNumberOfPhases(); i > 0; --i) - { - if (this.type != gameState.getPhaseName(i)) - continue; - gameState.ai.HQ.phasing = 0; - gameState.ai.HQ.OnPhaseUp(gameState, i); - break; - } -}; - -m.ResearchPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "rush": this.rush, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ResearchPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/queueplanTraining.js b/install/petraBased/petra-patriot/queueplanTraining.js deleted file mode 100644 index e63e387..0000000 --- a/install/petraBased/petra-patriot/queueplanTraining.js +++ /dev/null @@ -1,193 +0,0 @@ -var PETRA = function(m) -{ - -m.TrainingPlan = function(gameState, type, metadata, number = 1, maxMerge = 5) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - { - API3.warn(" Plan training " + type + " canceled"); - return false; - } - - // Refine the estimated cost and add pop cost - let trainers = this.getBestTrainers(gameState); - let trainer = trainers ? trainers[0] : undefined; - this.cost = new API3.Resources(this.template.cost(trainer), +this.template._template.Cost.Population); - - this.category = "unit"; - this.number = number; - this.maxMerge = maxMerge; - - return true; -}; - -m.TrainingPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.TrainingPlan.prototype.canStart = function(gameState) -{ - this.trainers = this.getBestTrainers(gameState); - if (!this.trainers) - return false; - this.cost = new API3.Resources(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); - return true; -}; - -m.TrainingPlan.prototype.getBestTrainers = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let trainer = gameState.getEntityById(this.metadata.trainer); - if (trainer) - return [trainer]; - } - - let allTrainers = gameState.findTrainers(this.type); - if (this.metadata && this.metadata.sea) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)); - if (this.metadata && this.metadata.base) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "base", this.metadata.base)); - if (!allTrainers || !allTrainers.hasEntities()) - return undefined; - - // Keep only trainers with smallest cost - let costMin = Math.min(); - let trainers; - for (let ent of allTrainers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - trainers.push(ent); - else if (cost < costMin) - { - costMin = cost; - trainers = [ent]; - } - } - return trainers; -}; - -m.TrainingPlan.prototype.start = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let metadata = {}; - for (let key in this.metadata) - if (key !== "trainer") - metadata[key] = this.metadata[key]; - this.metadata = metadata; - } - - if (this.trainers.length > 1) - { - let wantedIndex; - if (this.metadata && this.metadata.index) - wantedIndex = this.metadata.index; - let workerUnit = this.metadata && this.metadata.role && this.metadata.role == "worker"; - let supportUnit = this.template.hasClass("Support"); - this.trainers.sort(function(a, b) { - // Prefer training buildings with short queues - let aa = a.trainingQueueTime(); - let bb = b.trainingQueueTime(); - // Give priority to support units in the cc - if (a.hasClass("Civic") && !supportUnit) - aa += 10; - if (b.hasClass("Civic") && !supportUnit) - bb += 10; - // And support units should not be too near to dangerous place - if (supportUnit) - { - if (gameState.ai.HQ.isNearInvadingArmy(a.position())) - aa += 50; - if (gameState.ai.HQ.isNearInvadingArmy(b.position())) - bb += 50; - } - // Give also priority to buildings with the right accessibility - let aBase = a.getMetadata(PlayerID, "base"); - let bBase = b.getMetadata(PlayerID, "base"); - if (wantedIndex) - { - if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) - aa += 30; - if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) - bb += 30; - } - // Then, if workers, small preference for bases with less workers - if (workerUnit && aBase && bBase && aBase != bBase) - { - let apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; - let bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; - if (apop > bpop) - aa++; - else if (bpop > apop) - bb++; - } - return aa - bb; - }); - } - - if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) - this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); - this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState)); - - this.onStart(gameState); -}; - -m.TrainingPlan.prototype.addItem = function(amount = 1) -{ - this.number += amount; -}; - -/** Find the promoted types corresponding to this.type */ -m.TrainingPlan.prototype.promotedTypes = function(gameState) -{ - let types = []; - let promotion = this.template.promotion(); - let previous; - let template; - while (promotion) - { - types.push(promotion); - previous = promotion; - template = gameState.getTemplate(promotion); - if (!template) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" promotion template " + promotion + " is not found"); - promotion = undefined; - break; - } - promotion = template.promotion(); - if (previous === promotion) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" unit " + promotion + " is its own promoted unit"); - promotion = undefined; - } - } - return types; -}; - -m.TrainingPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "maxMerge": this.maxMerge - }; -}; - -m.TrainingPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/researchManager.js b/install/petraBased/petra-patriot/researchManager.js deleted file mode 100644 index 51ce3ff..0000000 --- a/install/petraBased/petra-patriot/researchManager.js +++ /dev/null @@ -1,244 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the research - */ - -m.ResearchManager = function(Config) -{ - this.Config = Config; -}; - -/** - * Check if we can go to the next phase - */ -m.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; - - let currentPhaseIndex = gameState.currentPhase(); - let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); - if (!nextPhaseName) - return; - - let 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 m.ResearchPlan(gameState, nextPhaseName, true)); - } -}; - -m.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 !== "Cost/PopulationBonus") - continue; - queues.minorTech.addPlan(new m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -m.ResearchManager.prototype.researchTradeBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -/** Techs to be searched for as soon as they are available */ -m.ResearchManager.prototype.researchWantedTechs = function(gameState, techs) -{ - let phase1 = gameState.currentPhase() === 1; - let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase1) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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 */ -m.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) -{ - let phase2 = gameState.currentPhase() === 2; - let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase2) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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; -}; - -m.ResearchManager.prototype.update = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - - let techName = this.researchWantedTechs(gameState, techs); - if (techName) - { - if (techName.increasePriority) - { - gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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) - { - let template = techs[i][1]._template; - if (template.affects && template.affects.length === 1 && - (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "StoneWall")) - { - techs.splice(i--, 1); - continue; - } - if (template.modifications && template.modifications.length === 1 && - template.modifications[0].value === "Player/sharedLos" && - !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 m.ResearchPlan(gameState, pickRandom(techs)[0])); -}; - -m.ResearchManager.prototype.CostSum = function(cost) -{ - let costSum = 0; - for (let res in cost) - costSum += cost[res]; - return costSum; -}; - -m.ResearchManager.prototype.Serialize = function() -{ - return {}; -}; - -m.ResearchManager.prototype.Deserialize = function(data) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/startingStrategy.js b/install/petraBased/petra-patriot/startingStrategy.js deleted file mode 100644 index db76f30..0000000 --- a/install/petraBased/petra-patriot/startingStrategy.js +++ /dev/null @@ -1,578 +0,0 @@ -var PETRA = function(m) -{ -/** - * determines the strategy to adopt when starting a new game, depending on the initial conditions - */ - -m.HQ.prototype.gameAnalysis = function(gameState) -{ - // Analysis of the terrain and the different access regions - if (!this.regionAnalysis(gameState)) - return; - - this.attackManager.init(gameState); - this.buildManager.init(gameState); - this.navalManager.init(gameState); - this.tradeManager.init(gameState); - this.diplomacyManager.init(gameState); - - // Make a list of buildable structures from the config file - this.structureAnalysis(gameState); - - // Let's get our initial situation here. - let nobase = new m.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); - this.updateTerritories(gameState); - - // Assign entities and resources in the different bases - this.assignStartingEntities(gameState); - - - // Sandbox difficulty should not try to expand - this.canExpand = this.Config.difficulty != 0; - // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks - this.canBuildUnits = true; - if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) - { - let template = gameState.applyCiv("structures/{civ}_civil_centre"); - if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) - { - if (this.Config.debug > 1) - API3.warn(" this AI is unable to produce any units"); - this.canBuildUnits = false; - this.dispatchUnits(gameState); - } - else - this.buildFirstBase(gameState); - } - - // configure our first base strategy - if (this.baseManagers.length > 1) - this.configFirstBase(gameState); -}; - -/** - * Assign the starting entities to the different bases - */ -m.HQ.prototype.assignStartingEntities = function(gameState) -{ - for (let ent of gameState.getOwnEntities().values()) - { - // do not affect merchant ship immediately to trade as they may-be useful for transport - if (ent.hasClass("Trader") && !ent.hasClass("Ship")) - this.tradeManager.assignTrader(ent); - - let pos = ent.position(); - if (!pos) - { - // TODO should support recursive garrisoning. Make a warning for now - if (ent.isGarrisonHolder() && ent.garrisoned().length) - API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); - continue; - } - - // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - if (land > 1 && !this.landRegions[land]) - this.landRegions[land] = true; - let sea = gameState.ai.accessibility.navalPassMap[index]; - if (sea > 1 && !this.navalRegions[sea]) - this.navalRegions[sea] = true; - - // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport - // when a construction will start (see createTransportIfNeeded) - if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) - for (let id of ent.garrisoned()) - ent.unload(id); - - let bestbase; - let territorypos = this.territoryMap.gamePosToMapPos(pos); - let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; - for (let i = 1; i < this.baseManagers.length; ++i) - { - let base = this.baseManagers[i]; - 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, "anchorless"); - else - bestbase = m.getBestBase(gameState, ent) || this.baseManagers[0]; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (pos && bestbase.ID !== this.baseManagers[0].ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === "worker") - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } - } -}; - -/** - * determine the main land Index (or water index if none) - * as well as the list of allowed (land andf water) regions - */ -m.HQ.prototype.regionAnalysis = function(gameState) -{ - let accessibility = gameState.ai.accessibility; - let landIndex; - let seaIndex; - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - let land = accessibility.getAccessValue(cc.position()); - if (land > 1) - { - landIndex = land; - break; - } - } - if (!landIndex) - { - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) - continue; - let land = accessibility.getAccessValue(ent.position()); - if (land > 1) - { - landIndex = land; - break; - } - let sea = accessibility.getAccessValue(ent.position(), true); - if (!seaIndex && sea > 1) - seaIndex = sea; - } - } - if (!landIndex && !seaIndex) - { - API3.warn("Petra error: it does not know how to interpret this map"); - return false; - } - - let passabilityMap = gameState.getPassabilityMap(); - let totalSize = passabilityMap.width * passabilityMap.width; - let minLandSize = Math.floor(0.1*totalSize); - let minWaterSize = Math.floor(0.2*totalSize); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - for (let i = 0; i < accessibility.regionSize.length; ++i) - { - if (landIndex && i == landIndex) - this.landRegions[i] = true; - else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) - { - if (landIndex) - { - let sea = this.getSeaBetweenIndices(gameState, landIndex, i); - if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[sea] = true; - } - } - else - { - let traject = accessibility.getTrajectToIndex(seaIndex, i); - if (traject && traject.length === 2) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[seaIndex] = true; - } - } - } - else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) - { - this.navalMap = true; - this.navalRegions[i] = true; - } - else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) - this.navalRegions[i] = true; - } - - if (this.Config.debug < 3) - return true; - for (let region in this.landRegions) - API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); - API3.warn(" navalMap " + this.navalMap); - API3.warn(" landRegions " + uneval(this.landRegions)); - API3.warn(" navalRegions " + uneval(this.navalRegions)); - return true; -}; - -/** - * load units and buildings from the config files - * TODO: change that to something dynamic - */ -m.HQ.prototype.structureAnalysis = function(gameState) -{ - let civref = gameState.playerData.civ; - let civ = civref in this.Config.buildings ? civref : 'default'; - this.bAdvanced = []; - for (let building of this.Config.buildings[civ]) - if (gameState.isTemplateAvailable(gameState.applyCiv(building))) - this.bAdvanced.push(gameState.applyCiv(building)); -}; - -/** - * build our first base - * if not enough resource, try first to do a dock - */ -m.HQ.prototype.buildFirstBase = function(gameState) -{ - if (gameState.ai.queues.civilCentre.hasQueuedUnits()) - return; - let templateName = gameState.applyCiv("structures/{civ}_civil_centre"); - if (gameState.isTemplateDisabled(templateName)) - return; - let template = gameState.getTemplate(templateName); - if (!template) - return; - let total = gameState.getResources(); - let goal = "civil_centre"; - if (!total.canAfford(new API3.Resources(template.cost()))) - { - let totalExpected = gameState.getResources(); - // Check for treasures around available in some maps at startup - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position()) - continue; - // If we can get a treasure around, just do it - if (ent.isIdle()) - m.gatherTreasure(gameState, ent); - // Then count the resources from the treasures being collected - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (!supplyId) - continue; - let supply = gameState.getEntityById(supplyId); - if (!supply || supply.resourceSupplyType().generic != "treasure") - continue; - let type = supply.resourceSupplyType().specific; - if (!(type in totalExpected)) - continue; - totalExpected[type] += supply.resourceSupplyMax(); - // If we can collect enough resources from these treasures, wait for them - if (totalExpected.canAfford(new API3.Resources(template.cost()))) - return; - } - - // not enough resource to build a cc, try with a dock to accumulate resources if none yet - if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) - { - if (gameState.ai.queues.dock.hasQueuedUnits()) - return; - templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.isTemplateDisabled(templateName)) - return; - template = gameState.getTemplate(templateName); - if (!template || !total.canAfford(new API3.Resources(template.cost()))) - return; - goal = "dock"; - } - } - if (!this.canBuild(gameState, templateName)) - return; - - // We first choose as startingPoint the point where we have the more units - let startingPoint = []; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) - continue; - if (ent.hasClass("Cavalry")) - continue; - let pos = ent.position(); - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - continue; - pos = holder.position(); - } - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - let sea = gameState.ai.accessibility.navalPassMap[index]; - let found = false; - for (let point of startingPoint) - { - if (land !== point.land || sea !== point.sea) - continue; - if (API3.SquareVectorDistance(point.pos, pos) > 2500) - continue; - point.weight += 1; - found = true; - break; - } - if (!found) - startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 }); - } - if (!startingPoint.length) - return; - - let imax = 0; - for (let i = 1; i < startingPoint.length; ++i) - if (startingPoint[i].weight > startingPoint[imax].weight) - imax = i; - - if (goal == "dock") - { - let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; - gameState.ai.queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); - } - else - gameState.ai.queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); -}; - -/** - * set strategy if game without construction: - * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack - * - otherwise all units will attack - */ -m.HQ.prototype.dispatchUnits = function(gameState) -{ - let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - if (allycc.length) - { - if (this.Config.debug > 1) - API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); - let units = gameState.getOwnUnits(); - let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); - let num1 = Math.floor(num / 2); - let num2 = num1; - // first pass to affect ranged infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => { - if (!num || !num1) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num1; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // second pass to affect melee infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => { - if (!num || !num2) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num2; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // and now complete the affectation, including all support units - units.forEach(ent => { - if (!num && !ent.hasClass("Support")) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - if (!ent.hasClass("Support")) - --num; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - } -}; - -/** - * configure our first base expansion - * - if on a small island, favor fishing - * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - */ -m.HQ.prototype.configFirstBase = function(gameState) -{ - if (this.baseManagers.length < 2) - return; - - this.firstBaseConfig = true; - - let startingSize = 0; - let startingLand = []; - for (let region in this.landRegions) - { - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex != +region) - continue; - startingSize += gameState.ai.accessibility.regionSize[region]; - startingLand.push(base.accessIndex); - break; - } - } - let cell = gameState.getPassabilityMap().cellSize; - startingSize = startingSize * cell * cell; - if (this.Config.debug > 1) - API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); - if (startingSize < 25000) - { - this.saveSpace = true; - this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); - let num = Math.max(this.Config.Economy.targetNumFishers, 2); - for (let land of startingLand) - { - for (let sea of gameState.ai.accessibility.regionLinks[land]) - if (gameState.ai.HQ.navalRegions[sea]) - this.navalManager.updateFishingBoats(sea, num); - } - this.maxFields = 1; - this.needCorral = true; - } - else if (startingSize < 60000) - this.maxFields = 2; - else - this.maxFields = false; - - // - count the available wood resource, and react accordingly - let startingFood = gameState.getResources().food; - let check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.food[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingFood += supply.ent.resourceSupplyAmount(); - } - } - } - if (startingFood < 800) - { - if (startingSize < 25000) - { - this.needFish = true; - this.Config.Economy.popForDock = 1; - } - else - this.needFarm = true; - } - // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - let startingWood = gameState.getResources().wood; - check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.wood[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingWood += supply.ent.resourceSupplyAmount(); - } - } - } - if (this.Config.debug > 1) - API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); - if (startingWood < 6000) - { - this.saveResources = true; - this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand - - if (startingWood < 2000 && this.needFarm) - { - this.needCorral = true; - this.needFarm = false; - } - } - if (startingWood > 8500 && this.canBuildUnits) - { - let allowed = Math.ceil((startingWood - 8500) / 3000); - // Not useful to prepare rushing if too long ceasefire - if (gameState.isCeasefireActive()) - { - if (gameState.ceasefireTimeRemaining > 900) - allowed = 0; - else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) - allowed = 1; - } - this.attackManager.setRushes(allowed); - } - - // immediatly build a wood dropsite if possible. - let template = gameState.applyCiv("structures/{civ}_storehouse"); - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) - { - let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); - if (newDP.quality > 40) - { - // if we start with enough workers, put our available resources in this first dropsite - // same thing if our pop exceed the allowed one, as we will need several houses - let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; - if (numWorkers > 12 && newDP.quality > 60 || - gameState.getPopulation() > gameState.getPopulationLimit() + 20) - { - let cost = new API3.Resources(gameState.getTemplate(template).cost()); - gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); - } - gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); - } - } - // and build immediately a corral if needed - if (this.needCorral) - { - template = gameState.applyCiv("structures/{civ}_corral"); - if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) - gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); - } -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-patriot/tradeManager.js b/install/petraBased/petra-patriot/tradeManager.js deleted file mode 100644 index 0878f6c..0000000 --- a/install/petraBased/petra-patriot/tradeManager.js +++ /dev/null @@ -1,723 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the trade - */ - -m.TradeManager = function(Config) -{ - this.Config = Config; - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - this.routeProspection = false; - this.targetNumTraders = this.Config.Economy.targetNumTraders; - this.warnedAllies = {}; -}; - -m.TradeManager.prototype.init = function(gameState) -{ - this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); - this.traders.registerUpdates(); - this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; -}; - -m.TradeManager.prototype.hasTradeRoute = function() -{ - return this.tradeRoute !== undefined; -}; - -m.TradeManager.prototype.assignTrader = function(ent) -{ - ent.setMetadata(PlayerID, "role", "trader"); - this.traders.updateEnt(ent); -}; - -m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) -{ - if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) - return; - - let numTraders = this.traders.length; - let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length; - let numLandTraders = numTraders - numSeaTraders; - // add traders already in training - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "trader") - continue; - numTraders += item.count; - if (item.metadata.sea !== undefined) - numSeaTraders += item.count; - else - numLandTraders += item.count; - } - }); - if (numTraders >= this.targetNumTraders && - (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || - this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) - return; - - let template; - let metadata = { "role": "trader" }; - if (this.tradeRoute.sea) - { - // if we have some merchand ships affected to transport, try first to reaffect them - // May-be, there were produced at an early stage when no other ship were available - // and the naval manager will train now more appropriate ships. - let already = false; - let shipToSwitch; - gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) { - if (already || !ship.hasClass("Trader")) - return; - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - { - already = true; - return; - } - shipToSwitch = ship; - }); - if (already) - return; - if (shipToSwitch) - { - if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) - shipToSwitch.setMetadata(PlayerID, "role", "trader"); - else - shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader"); - return; - } - - template = gameState.applyCiv("units/{civ}_ship_merchant"); - metadata.sea = this.tradeRoute.sea; - } - else - { - template = gameState.applyCiv("units/{civ}_support_trader"); - if (!this.tradeRoute.source.hasClass("NavalMarket")) - metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base"); - else - metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base"); - } - - if (!gameState.getTemplate(template)) - { - if (this.Config.debug > 0) - API3.warn("Petra error: trying to train " + template + " for civ " + - gameState.getPlayerCiv() + " but no template found."); - return; - } - queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1)); -}; - -m.TradeManager.prototype.updateTrader = function(gameState, ent) -{ - if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && - !ent.unitAIState().startsWith("INDIVIDUAL.GATHER") && - m.gatherTreasure(gameState, ent, true)) - return; - - if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) - return; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; - - // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain - - Engine.ProfileStart("Trade Manager"); - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let route = this.checkRoutes(gameState, access); - if (!route) - { - // TODO try to garrison land trader inside merchant ship when only sea routes available - if (this.Config.debug > 0) - API3.warn(" no available route for " + ent.genericName() + " " + ent.id()); - Engine.ProfileStop(); - return; - } - - let nearerSource = true; - if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position())) - nearerSource = false; - - if (!ent.hasClass("Ship") && route.land != access) - { - if (nearerSource) - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position()); - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position()); - Engine.ProfileStop(); - return; - } - - if (nearerSource) - ent.tradeRoute(route.target, route.source); - else - ent.tradeRoute(route.source, route.target); - ent.setMetadata(PlayerID, "route", this.routeEntToId(route)); - Engine.ProfileStop(); -}; - -m.TradeManager.prototype.setTradingGoods = function(gameState) -{ - let tradingGoods = {}; - for (let res of Resources.GetCodes()) - tradingGoods[res] = 0; - // first, try to anticipate future needs - let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); - let remaining = 100; - let targetNum = this.Config.Economy.targetNumTraders; - for (let res in stocks) - { - if (res == "food") - continue; - let wantedRate = wantedRates[res]; - if (stocks[res] < 200) - { - tradingGoods[res] = wantedRate > 0 ? 20 : 10; - targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); - } - else if (stocks[res] < 500) - { - tradingGoods[res] = wantedRate > 0 ? 15 : 10; - targetNum += 2; - } - else if (stocks[res] < 1000) - { - tradingGoods[res] = 10; - targetNum += 1; - } - remaining -= tradingGoods[res]; - } - this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); - - - // then add what is needed now - let mainNeed = Math.floor(remaining * 70 / 100); - let nextNeed = remaining - mainNeed; - - tradingGoods[mostNeeded[0].type] += mainNeed; - if (mostNeeded[1].wanted > 0) - tradingGoods[mostNeeded[1].type] += nextNeed; - else - tradingGoods[mostNeeded[0].type] += nextNeed; - Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); - if (this.Config.debug > 2) - API3.warn(" trading goods set to " + uneval(tradingGoods)); -}; - -/** - * Try to barter unneeded resources for needed resources. - * only once per turn because the info is not updated within a turn - */ -m.TradeManager.prototype.performBarter = function(gameState) -{ - let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); - if (barterers.length == 0) - return false; - - // Available resources after account substraction - let available = gameState.ai.queueManager.getAvailableResources(gameState); - let needs = gameState.ai.queueManager.currentNeeds(gameState); - - let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); - - let barterPrices = gameState.getBarterPrices(); - // calculates conversion rates - let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); - - // loop through each missing resource checking if we could barter and help finishing a queue quickly. - for (let buy of Resources.GetCodes()) - { - // 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 (let sell of Resources.GetCodes()) - { - 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; - } - - let barterRate = getBarterRate(barterPrices, buy, sell); - if (barterRate > bestRate && barterRate > barterRateMin) - { - bestRate = barterRate; - bestToSell = sell; - } - } - if (bestToSell !== undefined) - { - let amount = available[bestToSell] > 5000 ? 500 : 100; - barterers[0].barter(buy, bestToSell, amount); - if (this.Config.debug > 2) - API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + - " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + - " rate buy " + rates[buy] + " available sell " + available[bestToSell] + - " available buy " + available[buy] + " barterRate " + bestRate + - " 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) - return false; - let bestToBuy; - let bestChoice = 0; - for (let buy of Resources.GetCodes()) - { - if (buy == "food") - continue; - let barterRateMin = 80; - if (available[buy] < 5000 && available.food > 5000) - barterRateMin -= 20 - Math.floor(available[buy]/250); - let barterRate = getBarterRate(barterPrices, buy, "food"); - if (barterRate < barterRateMin) - continue; - let choice = barterRate / (100 + available[buy]); - if (choice > bestChoice) - { - bestChoice = choice; - bestToBuy = buy; - } - } - if (bestToBuy !== undefined) - { - let amount = available.food > 5000 ? 500 : 100; - barterers[0].barter(bestToBuy, "food", amount); - if (this.Config.debug > 2) - API3.warn("Contingency bartering: sold food for " + bestToBuy + - " available sell " + available.food + " available buy " + available[bestToBuy] + - " barterRate " + getBarterRate(barterPrices, bestToBuy, "food") + - " amount " + amount); - return true; - } - - return false; -}; - -m.TradeManager.prototype.checkEvents = function(gameState, events) -{ - // check if one market from a traderoute is renamed, change the route accordingly - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.hasClass("Market")) - continue; - for (let trader of this.traders.values()) - { - let route = trader.getMetadata(PlayerID, "route"); - if (!route) - continue; - if (route.source == evt.entity) - route.source = evt.newentity; - else if (route.target == evt.entity) - route.target = evt.newentity; - else - continue; - trader.setMetadata(PlayerID, "route", route); - } - } - - // if one market (or market-foundation) is destroyed, we should look for a better route - for (let evt of events.Destroy) - { - if (!evt.entityObj) - continue; - let ent = evt.entityObj; - if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - // same thing if one market is built - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || - !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - - // and same thing for captured markets - for (let evt of events.OwnershipChanged) - { - if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) - continue; - this.activateProspection(gameState); - return true; - } - - // or if diplomacy changed - if (events.DiplomacyChanged.length) - { - this.activateProspection(gameState); - return true; - } - - return false; -}; - -m.TradeManager.prototype.activateProspection = function(gameState) -{ - this.routeProspection = true; - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); -}; - -/** - * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute - * If an index is given, it returns the best route with this index or the best land route if index is a land index - */ -m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) -{ - let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); - let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); - if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon - { - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - return false; - } - - let onlyOurs = !market2.hasEntities(); - if (onlyOurs) - market2 = market1; - let candidate = { "gain": 0 }; - let potential = { "gain": 0 }; - let bestIndex = { "gain": 0 }; - let bestLand = { "gain": 0 }; - - let mapSize = gameState.sharedScript.mapSize; - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let m1 of market1.values()) - { - if (!m1.position()) - continue; - let access1 = m.getLandAccess(gameState, m1); - let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined; - for (let m2 of market2.values()) - { - if (onlyOurs && m1.id() >= m2.id()) - continue; - if (!m2.position()) - continue; - let access2 = m.getLandAccess(gameState, m2); - let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined; - let land = access1 == access2 ? access1 : undefined; - let sea = sea1 && sea1 == sea2 ? sea1 : undefined; - if (!land && !sea) - continue; - if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) - continue; - let gainMultiplier; - if (land && traderTemplatesGains.landGainMultiplier) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else if (sea && traderTemplatesGains.navalGainMultiplier) - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - else - continue; - let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize)); - if (gain < this.minimalGain) - continue; - if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) - { - if (accessIndex) - { - if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land") - { - if (gain < bestLand.gain) - continue; - bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - if (gain < candidate.gain) - continue; - candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - if (gain < potential.gain) - continue; - potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - - if (potential.gain < 1) - this.potentialTradeRoute = undefined; - else - this.potentialTradeRoute = potential; - - if (candidate.gain < 1) - { - if (this.Config.debug > 2) - API3.warn("no better trade route possible"); - this.tradeRoute = undefined; - return false; - } - - if (this.Config.debug > 1 && this.tradeRoute) - { - if (candidate.gain > this.tradeRoute.gain) - API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain); - } - else if (this.Config.debug > 1) - API3.warn("one trade route set with gain " + candidate.gain); - this.tradeRoute = candidate; - - if (this.Config.chat) - { - let owner = this.tradeRoute.source.owner(); - if (owner == PlayerID) - owner = this.tradeRoute.target.owner(); - if (owner != PlayerID && !this.warnedAllies[owner]) - { // Warn an ally that we have a trade route with him - m.chatNewTradeRoute(gameState, owner); - this.warnedAllies[owner] = true; - } - } - - if (accessIndex) - { - if (bestIndex.gain > 0) - return bestIndex; - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) - return bestLand; - return false; - } - return true; -}; - -/** Called when a market was built or destroyed, and checks if trader orders should be changed */ -m.TradeManager.prototype.checkTrader = function(gameState, ent) -{ - let presentRoute = ent.getMetadata(PlayerID, "route"); - if (!presentRoute) - return; - - if (!ent.position()) - { - // This trader is garrisoned, we will decide later (when ungarrisoning) what to do - ent.setMetadata(PlayerID, "route", undefined); - return; - } - - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let possibleRoute = this.checkRoutes(gameState, access); - // Warning: presentRoute is from metadata, so contains entity ids - if (!possibleRoute || - possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target || - possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target) - { - // Trader will be assigned in updateTrader - ent.setMetadata(PlayerID, "route", undefined); - if (!possibleRoute && !ent.hasClass("Ship")) - { - let closestBase = m.getBestBase(gameState, ent, true); - if (closestBase.accessIndex == access) - { - let closestBasePos = closestBase.anchor.position(); - ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); - return; - } - } - ent.stopMoving(); - } -}; - -m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) -{ - if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market")) - return; - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) - return; - if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() && - !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities()) - return; - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market")); - if (!template) - return; - this.checkRoutes(gameState); - let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); - if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain - { // no position found - if (gameState.getOwnEntitiesByClass("BarterMarket", 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) - API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + - marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); - else - API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + - marketPos[3]); - } - - if (!this.tradeRoute) - gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - if (!this.tradeRoute) - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) -{ - if (expectedGain < this.minimalGain) - return false; - if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && - expectedGain < this.potentialTradeRoute.gain + 20) - return false; - return true; -}; - -m.TradeManager.prototype.update = function(gameState, events, queues) -{ - if (gameState.ai.HQ.canBarter) - this.performBarter(gameState); - - if (this.Config.difficulty <= 1) - return; - - if (this.checkEvents(gameState, events)) // true if one market was built or destroyed - { - this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); - this.checkRoutes(gameState); - } - - if (this.tradeRoute) - { - this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); - if (gameState.ai.playedTurn % 5 == 0) - this.trainMoreTraders(gameState, queues); - if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) - gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); - if (gameState.ai.playedTurn % 60 == 0) - this.setTradingGoods(gameState); - } - - if (this.routeProspection) - this.prospectForNewMarket(gameState, queues); -}; - -m.TradeManager.prototype.routeEntToId = function(route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - if (!route[key]) - return undefined; - ret[key] = route[key].id(); - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.routeIdToEnt = function(gameState, route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - ret[key] = gameState.getEntityById(route[key]); - if (!ret[key]) - return undefined; - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.Serialize = function() -{ - return { - "tradeRoute": this.routeEntToId(this.tradeRoute), - "potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute), - "routeProspection": this.routeProspection, - "targetNumTraders": this.targetNumTraders, - "warnedAllies": this.warnedAllies - }; -}; - -m.TradeManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - { - if (key == "tradeRoute" || key == "potentialTradeRoute") - this[key] = this.routeIdToEnt(gameState, data[key]); - else - this[key] = data[key]; - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/transportPlan.js b/install/petraBased/petra-patriot/transportPlan.js deleted file mode 100644 index ee33a15..0000000 --- a/install/petraBased/petra-patriot/transportPlan.js +++ /dev/null @@ -1,729 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Describes a transport plan - * Constructor assign units (units is an ID array), a destination (position). - * The naval manager will try to deal with it accordingly. - * - * By this I mean that the naval manager will find how to go from access point 1 to access point 2 - * and then carry units from there. - * - * Note: only assign it units currently over land, or it won't work. - * Also: destination should probably be land, otherwise the units will be lost at sea. - * - * metadata for units: - * transport = this.ID - * onBoard = ship.id() when affected to a ship but not yet garrisoned - * = "onBoard" when garrisoned in a ship - * = undefined otherwise - * endPos = position of destination - * - * metadata for ships - * transporter = this.ID - */ - -m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) -{ - this.ID = gameState.ai.uniqueIDs.transports++; - this.debug = gameState.ai.Config.debug; - this.flotilla = false; // when false, only one ship per transport ... not yet tested when true - - this.endPos = endPos; - this.endIndex = endIndex; - this.startIndex = startIndex; - // TODO only cases with land-sea-land are allowed for the moment - // we could also have land-sea-land-sea-land - if (startIndex == 1) - { - // special transport from already garrisoned ship - if (!ship) - { - this.failed = true; - return false; - } - this.sea = ship.getMetadata(PlayerID, "sea"); - ship.setMetadata(PlayerID, "transporter", this.ID); - ship.setStance("none"); - for (let 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) - API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); - return false; - } - } - - for (let ent of units) - { - ent.setMetadata(PlayerID, "transport", this.ID); - ent.setMetadata(PlayerID, "endPos", endPos); - } - - if (this.debug > 1) - API3.warn("Starting a new transport plan with ID " + this.ID + - " to index " + endIndex + " with units length " + units.length); - - this.state = "boarding"; - this.boardingPos = {}; - this.needTransportShips = ship === undefined; - this.nTry = {}; - return true; -}; - -m.TransportPlan.prototype.init = function(gameState) -{ - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "transport", this.ID)); - this.ships = gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - - this.units.registerUpdates(); - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - - this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range -}; - -/** count available slots */ -m.TransportPlan.prototype.countFreeSlots = function() -{ - let slots = 0; - for (let ship of this.transportShips.values()) - slots += this.countFreeSlotsOnShip(ship); - return slots; -}; - -m.TransportPlan.prototype.countFreeSlotsOnShip = function(ship) -{ - if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) - return 0; - let occupied = ship.garrisoned().length + - this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", ship.id())).length; - return Math.max(ship.garrisonMax() - occupied, 0); -}; - -m.TransportPlan.prototype.assignUnitToShip = function(gameState, ent) -{ - if (this.needTransportShips) - return; - - for (let ship of this.transportShips.values()) - { - if (this.countFreeSlotsOnShip(ship) == 0) - continue; - ent.setMetadata(PlayerID, "onBoard", ship.id()); - if (this.debug > 1) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - } - return; - } - - if (this.flotilla) - { - this.needTransportShips = true; - return; - } - - if (!this.needSplit) - this.needSplit = [ent]; - else - this.needSplit.push(ent); -}; - -m.TransportPlan.prototype.assignShip = function(gameState) -{ - let pos; - // choose a unit of this plan not yet assigned to a ship - for (let ent of this.units.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "onBoard") !== undefined) - continue; - pos = ent.position(); - break; - } - // and choose the nearest available ship from this unit - let distmin = Math.min(); - let nearest; - gameState.ai.HQ.navalManager.seaTransportShips[this.sea].forEach(ship => { - if (ship.getMetadata(PlayerID, "transporter")) - return; - if (pos) - { - let dist = API3.SquareVectorDistance(pos, ship.position()); - if (dist > distmin) - return; - distmin = dist; - nearest = ship; - } - else if (!nearest) - nearest = ship; - }); - if (!nearest) - return false; - - nearest.setMetadata(PlayerID, "transporter", this.ID); - nearest.setStance("none"); - this.ships.updateEnt(nearest); - this.transportShips.updateEnt(nearest); - this.needTransportShips = false; - return true; -}; - -/** add a unit to this plan */ -m.TransportPlan.prototype.addUnit = function(unit, endPos) -{ - unit.setMetadata(PlayerID, "transport", this.ID); - unit.setMetadata(PlayerID, "endPos", endPos); - this.units.updateEnt(unit); -}; - -/** remove a unit from this plan, if not yet on board */ -m.TransportPlan.prototype.removeUnit = function(gameState, unit) -{ - let 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); - let ship = gameState.getEntityById(shipId); - if (ship && !ship.garrisoned().length && - !this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", shipId)).length) - { - this.releaseShip(ship); - this.ships.updateEnt(ship); - this.transportShips.updateEnt(ship); - } - } -}; - -m.TransportPlan.prototype.releaseShip = function(ship) -{ - if (ship.getMetadata(PlayerID, "transporter") != this.ID) - { - API3.warn(" Petra: try removing a transporter ship with " + ship.getMetadata(PlayerID, "transporter") + - " from " + this.ID + " and stance " + ship.getStance()); - return; - } - - let defaultStance = ship.get("UnitAI/DefaultStance"); - if (defaultStance) - ship.setStance(defaultStance); - - ship.setMetadata(PlayerID, "transporter", undefined); - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - ship.setMetadata(PlayerID, "role", "trader"); -}; - -m.TransportPlan.prototype.releaseAll = function() -{ - for (let ship of this.ships.values()) - this.releaseShip(ship); - - for (let 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 */ -m.TransportPlan.prototype.cancelTransport = function(gameState) -{ - let ent = this.units.toEntityArray()[0]; - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchor || !base.anchor.position()) - { - for (let newbase of gameState.ai.HQ.baseManagers) - { - if (!newbase.anchor || !newbase.anchor.position()) - continue; - ent.setMetadata(PlayerID, "base", newbase.ID); - base = newbase; - break; - } - if (!base.anchor || !base.anchor.position()) - return false; - this.units.forEach(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. There are two states: - * - "boarding" means we're trying to board units onto our ships - * - "sailing" means we're moving ships and eventually unload units - * - then the plan is cleared - */ - -m.TransportPlan.prototype.update = function(gameState) -{ - if (this.state == "boarding") - this.onBoarding(gameState); - else if (this.state == "sailing") - this.onSailing(gameState); - - return this.units.length; -}; - -m.TransportPlan.prototype.onBoarding = function(gameState) -{ - let ready = true; - let time = gameState.ai.elapsedTime; - let shipTested = {}; - - for (let ent of this.units.values()) - { - if (!ent.getMetadata(PlayerID, "onBoard")) - { - ready = false; - this.assignUnitToShip(gameState, ent); - if (ent.getMetadata(PlayerID, "onBoard")) - { - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let 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; - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let ship = gameState.getEntityById(shipId); - if (!ship) // the ship must have been destroyed - { - ent.setMetadata(PlayerID, "onBoard", undefined); - continue; - } - let distShip = API3.SquareVectorDistance(this.boardingPos[shipId], ship.position()); - if (!shipTested[shipId] && distShip > this.boardingRange) - { - shipTested[shipId] = true; - let retry = false; - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.WALKING" || - unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") - { - if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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, 30); - ent.garrison(ship, true); - } - else if (API3.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 (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.state = "sailing"; - this.nTry = {}; - this.unloaded = []; - this.recovered = []; -}; - -/** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ -m.TransportPlan.prototype.isOnBoard = function(ent) -{ - for (let ship of this.transportShips.values()) - { - if (ship.garrisoned().indexOf(ent.id()) == -1) - continue; - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - return true; - } - return false; -}; - -/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ -m.TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, avoidEnnemy) -{ - if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex); - return destination; - } - else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); - return destination; - } - - let startPos = ship.position(); - let distmin = Math.min(); - let posmin = destination; - let width = gameState.getPassabilityMap().width; - let cell = gameState.getPassabilityMap().cellSize; - let alliedDocks = gameState.getAllyStructures().filter(API3.Filters.and( - API3.Filters.byClass("Dock"), API3.Filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); - for (let i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - let pos = [i%width+0.5, Math.floor(i/width)+0.5]; - pos = [cell*pos[0], cell*pos[1]]; - let dist = API3.VectorDistance(startPos, pos); - if (destination) - dist += API3.VectorDistance(pos, destination); - if (avoidEnnemy) - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - dist += 100000000; - } - // require a small distance between all ships of the transport plan to avoid path finder problems - // this is also used when the ship is blocked and we want to find a new boarding point - for (let shipId in this.boardingPos) - if (this.boardingPos[shipId] !== undefined && - API3.SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) - dist += 1000000; - // and not too near our allied docks to not disturb naval traffic - let distSquare; - for (let dock of alliedDocks) - { - if (dock.foundationProgress() !== undefined) - distSquare = 900; - else - distSquare = 4900; - let dockDist = API3.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; -}; - -m.TransportPlan.prototype.onSailing = function(gameState) -{ - // Check that the units recovered on the previous turn have been reloaded - for (let recov of this.recovered) - { - let ent = gameState.getEntityById(recov.entId); - if (!ent) // entity destroyed - continue; - if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again - { - let ship = gameState.getEntityById(recov.shipId); - if (ship) - ship.moveApart(recov.entPos, 15); - continue; - } - if (this.debug > 1) - API3.warn(">>> transport " + this.ID + " reloading failed ... <<<"); - // destroy the unit if inaccessible otherwise leave it there - let index = m.getLandAccess(gameState, ent); - if (gameState.ai.HQ.landRegions[index]) - { - if (this.debug > 1) - API3.warn(" recovered entity kept " + ent.id()); - this.resetUnit(gameState, ent); - // TODO we should not destroy it, but now the unit could still be reloaded on the next turn - // and mess everything - ent.destroy(); - } - else - { - if (this.debug > 1) - API3.warn("recovered entity destroyed " + ent.id()); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - this.recovered = []; - - // Check that the units unloaded on the previous turn have been really unloaded and in the right position - let shipsToMove = {}; - for (let entId of this.unloaded) - { - let ent = gameState.getEntityById(entId); - if (!ent) // entity destroyed - continue; - else if (!ent.position()) // unloading failed - { - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship) - { - if (ship.garrisoned().indexOf(entId) != -1) - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - else - { - API3.warn("Petra transportPlan problem: unit not on ship without position ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - API3.warn("Petra transportPlan problem: unit on ship, but no ship ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else if (m.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) - API3.warn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship && !this.canceled) - { - shipsToMove[ship.id()] = ship; - this.recovered.push({ "entId": ent.id(), "entPos": ent.position(), "shipId": ship.id() }); - ent.garrison(ship); - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - } - else - { - if (this.debug > 1) - API3.warn("no way ... we destroy it"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - // And make some room for other units - let pos = ent.position(); - let goal = ent.getMetadata(PlayerID, "endPos"); - let dist = goal ? API3.VectorDistance(pos, goal) : 0; - if (dist > 30) - ent.moveToRange(goal[0], goal[1], dist-20, dist-20); - else - ent.moveToRange(pos[0], pos[1], 20, 20); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - } - } - for (let shipId in shipsToMove) - { - this.boardingPos[shipId] = this.getBoardingPos(gameState, shipsToMove[shipId], this.endIndex, this.sea, this.endPos, true); - shipsToMove[shipId].move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - this.unloaded = []; - - if (this.canceled) - { - for (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.canceled = undefined; - } - - for (let ship of this.transportShips.values()) - { - if (ship.unitAIState() == "INDIVIDUAL.WALKING") - continue; - let shipId = ship.id(); - let dist = API3.SquareVectorDistance(ship.position(), this.boardingPos[shipId]); - let remaining = 0; - for (let entId of ship.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (!ent.getMetadata(PlayerID, "transport")) - continue; - remaining++; - if (dist < 625) - { - ship.unload(entId); - this.unloaded.push(entId); - ent.setMetadata(PlayerID, "onBoard", shipId); - } - } - - let recovering = 0; - for (let recov of this.recovered) - if (recov.shipId == shipId) - recovering++; - - if (!remaining && !recovering) // when empty, release the ship and move apart to leave room for other ships. TODO fight - { - ship.moveApart(this.boardingPos[shipId], 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) - API3.warn(shipId + " new attempt for a landing point "); - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); - } - ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - } -}; - -m.TransportPlan.prototype.resetUnit = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // if from an army or attack, remove it - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - if (ent.getMetadata(PlayerID, "PartOfArmy")) - { - let army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); - if (army) - army.removeOwn(gameState, ent.id()); - } -}; - -m.TransportPlan.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "flotilla": this.flotilla, - "endPos": this.endPos, - "endIndex": this.endIndex, - "startIndex": this.startIndex, - "sea": this.sea, - "state": this.state, - "boardingPos": this.boardingPos, - "needTransportShips": this.needTransportShips, - "nTry": this.nTry, - "canceled": this.canceled, - "unloaded": this.unloaded, - "recovered": this.recovered - }; -}; - -m.TransportPlan.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; - - this.failed = false; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/victoryManager.js b/install/petraBased/petra-patriot/victoryManager.js deleted file mode 100644 index e4a0453..0000000 --- a/install/petraBased/petra-patriot/victoryManager.js +++ /dev/null @@ -1,748 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Handle events that are important to specific victory conditions: - * in capture_the_relic, capture gaia relics and train military guards. - * in regicide, train healer and military guards for the hero. - * in wonder, train military guards. - */ - -m.VictoryManager = function(Config) -{ - 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. - */ -m.VictoryManager.prototype.init = function(gameState) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) - this.criticalEnts.set(wonder.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let hero of gameState.getOwnEntitiesByClass("Hero", true).values()) - { - let defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; - if (hero.getStance() != defaultStance) - hero.setStance(defaultStance); - this.criticalEnts.set(hero.id(), { - "garrisonEmergency": false, - "healersAssigned": 0, - "guardsAssigned": 0, // for non-healer guards - "guards": new Map() // ids of ents who are currently guarding this hero - }); - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - for (let relic of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).values()) - { - if (relic.owner() == PlayerID) - this.criticalEnts.set(relic.id(), { "guardsAssigned": 0, "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 - */ -m.VictoryManager.prototype.checkEvents = function(gameState, events) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let evt of events.Create) - { - let 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. - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - let builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); - if (builders) - for (let worker of builders.values()) - { - worker.setMetadata(PlayerID, "base", base.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - } - } - - for (let evt of events.ConstructionFinished) - { - if (!evt || !evt.newentity) - continue; - - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) - this.criticalEnts.set(ent.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let evt of events.Attacked) - { - if (!this.criticalEnts.has(evt.target)) - continue; - - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) - continue; - - let plan = target.getMetadata(PlayerID, "plan"); - let hero = this.criticalEnts.get(evt.target); - if (plan != -2 && plan != -3) - { - target.stopMoving(); - - if (plan >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); - if (attackPlan) - attackPlan.removeUnit(target, true); - } - - if (target.getMetadata(PlayerID, "PartOfArmy")) - { - let 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 (let evt of events.TrainingFinished) - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") == "criticalEntHealer") - this.assignGuardToCriticalEnt(gameState, ent); - } - - for (let evt of events.Garrison) - { - if (!this.criticalEnts.has(evt.entity)) - continue; - - let hero = this.criticalEnts.get(evt.entity); - if (hero.garrisonEmergency) - hero.garrisonEmergency = false; - - let holderEnt = gameState.getEntityById(evt.holder); - if (!holderEnt) - continue; - - if (holderEnt.hasClass("Ship")) - { - // If the hero is garrisoned on a ship, remove its guards - for (let guardId of hero.guards.keys()) - { - let 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 (let guardId of hero.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt) - continue; - - let 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; - - let pos = holderEnt.position(); - let radius = holderEnt.obstructionRadius().max; - if (pos) - guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); - } - } - } - - for (let evt of events.EntityRenamed) - { - if (!this.guardEnts.has(evt.entity)) - continue; - for (let 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 (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() != PlayerID) - continue; - - let entId = evt.entityObj.id(); - if (this.criticalEnts.has(entId)) - { - this.removeCriticalEnt(gameState, entId); - continue; - } - - if (!this.guardEnts.has(entId)) - continue; - - for (let data of this.criticalEnts.values()) - if (data.guards.has(entId)) - { - data.guards.delete(entId); - if (evt.entityObj.hasClass("Healer")) - --data.healersAssigned; - else - --data.guardsAssigned; - break; - } - - this.guardEnts.delete(entId); - } - - for (let evt of events.UnGarrison) - { - if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) - continue; - - let 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") == "criticalEntHealer" || - ent.getMetadata(PlayerID, "role") == "criticalEntGuard") && !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 - let criticalEnt = this.criticalEnts.get(evt.entity); - for (let [id, isGuarding] of this.guardEnts) - { - if (criticalEnt.guards.size >= this.healersPerCriticalEnt) - break; - - if (!isGuarding) - { - let guardEnt = gameState.getEntityById(id); - if (guardEnt) - this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); - } - } - } - - for (let 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; - - let 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": 0, "guards": new Map() }); - // Move captured relics to the closest base - if (ent.hasClass("Relic")) - this.pickCriticalEntRetreatLocation(gameState, ent, false); - } - } -}; - -m.VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId) -{ - for (let [guardId, role] of this.criticalEnts.get(criticalEntId).guards) - { - let 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 - */ -m.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 (let data of this.criticalEnts.values()) - { - if (data.healersAssigned === undefined || data.healersAssigned >= this.healersPerCriticalEnt) - continue; - let template = gameState.applyCiv("units/{civ}_support_healer_b"); - queues.healer.addPlan(new m.TrainingPlan(gameState, template, { "role": "criticalEntHealer", "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. - */ -m.VictoryManager.prototype.manageCriticalEntGuards = function(gameState) -{ - let numWorkers = gameState.getOwnEntitiesByRole("worker", true).length; - if (numWorkers < 20) - { - for (let data of this.criticalEnts.values()) - { - for (let guardId of data.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || - guardEnt.getMetadata(PlayerID, "role") != "criticalEntGuard") - continue; - - guardEnt.removeGuard(); - guardEnt.setMetadata(PlayerID, "plan", -1); - guardEnt.setMetadata(PlayerID, "role", undefined); - this.guardEnts.delete(guardId); - --data.guardsAssigned; - - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - - if (++numWorkers >= 20) - break; - } - if (numWorkers >= 20) - break; - } - } - - let minWorkers = 25; - let deltaWorkers = 3; - for (let [id, data] of this.criticalEnts) - { - let criticalEnt = gameState.getEntityById(id); - if (!criticalEnt) - continue; - - let militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + - Math.round(this.Config.personality.defensive * 5); - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt) - continue; - - // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports - for (let checkForSameAccess of [true, false]) - { - // First try to assign any Champion units we might have - for (let entity of gameState.getOwnEntitiesByClass("Champion", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.ai.HQ.attackManager.outOfPlan.values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - } -}; - -m.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() || - m.getLandAccess(gameState, criticalEnt) != m.getLandAccess(gameState, guardEnt))) - return false; - - if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) - return false; - - guardEnt.setMetadata(PlayerID, "plan", -2); - guardEnt.setMetadata(PlayerID, "role", "criticalEntGuard"); - return true; -}; - -m.VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency) -{ - gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); - let 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"); - let accessIndex = m.getLandAccess(gameState, criticalEnt); - let bestBase = m.getBestBase(gameState, criticalEnt, true); - if (bestBase.accessIndex == accessIndex) - { - let bestBasePos = bestBase.anchor.position(); - criticalEnt.move(bestBasePos[0], bestBasePos[1]); - } -}; - -/** - * 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). - */ -m.VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId) -{ - if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) - return false; - - if (criticalEntId && !this.criticalEnts.has(criticalEntId)) - { - criticalEntId = undefined; - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - } - - if (!criticalEntId) - { - let isHealer = guardEnt.hasClass("Healer"); - - // Assign to the critical ent with the fewest guards - let min = Math.min(); - for (let [id, data] of this.criticalEnts) - { - if (isHealer && (data.healersAssigned === undefined || data.healersAssigned > min)) - continue; - if (!isHealer && data.guardsAssigned > min) - continue; - - criticalEntId = id; - min = isHealer ? data.healersAssigned : data.guardsAssigned; - } - if (criticalEntId) - { - let data = this.criticalEnts.get(criticalEntId); - if (isHealer) - ++data.healersAssigned; - else - ++data.guardsAssigned; - } - } - - if (!criticalEntId) - { - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - return false; - } - - let 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); - - let guardEntAccess = m.getLandAccess(gameState, guardEnt); - let criticalEntAccess = m.getLandAccess(gameState, criticalEnt); - if (guardEntAccess == criticalEntAccess) - { - let queued = m.returnResources(gameState, guardEnt); - guardEnt.guard(criticalEnt, queued); - let guardRole = guardEnt.getMetadata(PlayerID, "role") == "criticalEntHealer" ? "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; -}; - -m.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; -}; - -m.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 (let id of this.criticalEnts.keys()) - { - let 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 (let relicId of this.targetedGaiaRelics.keys()) - { - let 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 - let allGaiaRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == 0); - for (let relic of allGaiaRelics.values()) - { - let relicPosition = relic.position(); - if (!relicPosition || this.targetedGaiaRelics.has(relic.id())) - continue; - let 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 (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position() || !ent.visionRange()) - continue; - if (API3.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. - */ -m.VictoryManager.prototype.captureGaiaRelic = function(gameState, relic) -{ - let capture = -relic.defaultRegenRate(); - let sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b); - let plans = this.targetedGaiaRelics.get(relic.id()); - for (let plan of plans) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (!attack) - continue; - for (let ent of attack.unitCollection.values()) - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - } - // No need to make a new attack if already enough units - if (capture > sumCapturePoints / 50) - return; - let relicPosition = relic.position(); - let access = m.getLandAccess(gameState, relic); - let 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; - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan == -2 || plan == -3) - return false; - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && (attack.state != "unexecuted" || attack.type == "Raid")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(relicPosition); - let expedition = []; - for (let ent of units.values()) - { - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - expedition.push(ent); - if (capture > sumCapturePoints / 25) - break; - } - if (!expedition.length || !plans.length && capture < sumCapturePoints / 100) - return; - let attack = gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); - if (!attack) - return; - let plan = attack.name; - attack.rallyPoint = undefined; - for (let 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); -}; - -m.VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId) -{ - for (let plan of this.targetedGaiaRelics.get(relicId)) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack) - attack.Abort(gameState); - } - this.targetedGaiaRelics.delete(relicId); -}; - -m.VictoryManager.prototype.Serialize = function() -{ - return { - "criticalEnts": this.criticalEnts, - "guardEnts": this.guardEnts, - "healersPerCriticalEnt": this.healersPerCriticalEnt, - "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, - "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, - "targetedGaiaRelics": this.targetedGaiaRelics - }; -}; - -m.VictoryManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-patriot/worker.js b/install/petraBased/petra-patriot/worker.js deleted file mode 100644 index bb4e2b5..0000000 --- a/install/petraBased/petra-patriot/worker.js +++ /dev/null @@ -1,1114 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This class makes a worker do as instructed by the economy manager - */ - -m.Worker = function(base) -{ - this.ent = undefined; - this.base = base; - this.baseID = base.ID; -}; - -m.Worker.prototype.update = function(gameState, ent) -{ - if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return; - - let subrole = ent.getMetadata(PlayerID, "subrole"); - - // If we are waiting for a transport or we are sailing, just wait - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board - if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target && plan && plan.state == "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 == "gatherer" || subrole == "hunter") - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan.state == "boarding" && ent.position()) - { - let hasDropsite = false; - let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; - for (let structure of gameState.getOwnStructures().values()) - { - if (m.getLandAccess(gameState, structure) != plan.endIndex) - continue; - let resourceDropsiteTypes = m.getBuiltEntity(gameState, structure).resourceDropsiteTypes(); - if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) - continue; - hasDropsite = true; - break; - } - if (!hasDropsite) - { - for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values()) - { - if (!unit.position() || m.getLandAccess(gameState, unit) != plan.endIndex) - continue; - let resourceDropsiteTypes = unit.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 = m.getLandAccess(gameState, ent); - // base 0 for unassigned entities has no accessIndex, so take the one from the entity - if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) - this.baseAccess = this.entAccess; - else - this.baseAccess = this.base.accessIndex; - - if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... - { - ent.setMetadata(PlayerID, "subrole", "idle"); - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - - this.ent = ent; - - let unitAIState = ent.unitAIState(); - if ((subrole == "hunter" || subrole == "gatherer") && - (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) - { - if (this.isInaccessibleSupply(gameState)) - { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); - } - - if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - // Check that we have not drifted too far when hunting - let target = gameState.getEntityById(orderData.target); - if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") - { - let 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)) - { - let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000; - let targetAccess = m.getLandAccess(gameState, target); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - let hasFoodDropsiteWithinDistance = false; - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (targetAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.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); - } - - let 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 == "fisher") - this.startFishing(gameState); - else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && - !ent.getMetadata(PlayerID, "PartOfArmy")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - let target = gameState.getEntityById(orderData.target); - if (target && (!target.position() || m.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")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") - { - // If we are here, an enemy structure must have targeted one of our workers - // and UnitAI sent it fight back with allowCapture=true - let target = gameState.getEntityById(orderData.target); - if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) - ent.attack(orderData.target, m.allowCapture(gameState, ent, target)); - } - } - return; - } - - // Okay so we have a few tasks. - // If we're gathering, we'll check that we haven't run idle. - // And we'll also check that we're gathering a resource we want to gather. - - if (subrole == "gatherer") - { - if (ent.isIdle()) - { - // if we aren't storing resources or it's the same type as what we're about to gather, - // let's just pick a new resource. - // TODO if we already carry the max we can -> returnresources - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || - ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) - { - this.startGathering(gameState); - } - else if (!m.returnResources(gameState, ent)) // try to deposit resources - { - // no dropsite, abandon old resources and start gathering new ones - this.startGathering(gameState); - } - } - else if (unitAIStateOrder == "GATHER") - { - // we're already gathering. But let's check if there is nothing better - // in case UnitAI did something bad - if (ent.unitAIOrderData().length) - { - let supplyId = ent.unitAIOrderData()[0].target; - let supply = gameState.getEntityById(supplyId); - if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") && - supply.resourceSupplyType().generic != "treasure" && - supplyId != ent.getMetadata(PlayerID, "supply")) - { - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); - if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (nearby.some(sup => sup.id == supplyId)) - ent.setMetadata(PlayerID, "supply", supplyId); - else if (nearby.length) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (medium.length && !medium.some(sup => sup.id == supplyId)) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - ent.setMetadata(PlayerID, "supply", supplyId); - } - } - } - } - } - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (gameState.ai.playedTurn % 10 == 0) - { - // Check from time to time that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, this.ent); - } - - // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. - // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be - // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; - if (influenceGroup && influenceGroup == "sparse") - { - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (supplyId) - { - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (!nearby.some(sup => sup.id == supplyId)) - { - if (nearby.length) - ent.setMetadata(PlayerID, "supply", undefined); - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (!medium.some(sup => sup.id == supplyId) && medium.length) - ent.setMetadata(PlayerID, "supply", undefined); - } - } - } - } - } - } - else if (subrole == "builder") - { - if (unitAIStateOrder == "REPAIR") - { - // Update our target in case UnitAI sent us to a different foundation because of autocontinue - // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) - if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && - ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) - { - let targetId = ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (target && !target.hasClass("Field")) - { - ent.setMetadata(PlayerID, "target-foundation", targetId); - return; - } - ent.setMetadata(PlayerID, "target-foundation", undefined); - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) - let targetId = ent.getMetadata(PlayerID, "target-foundation"); - if (targetId && gameState.getEntityById(targetId)) - return; - ent.stopMoving(); - } - // okay so apparently we aren't working. - // Unless we've been explicitely told to keep our role, make us idle. - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.setMetadata(PlayerID, "target-foundation", undefined); - // If worker elephant, move away to avoid being trapped in between constructions - if (ent.hasClass("Elephant")) - this.moveToGatherer(gameState, ent, true); - else if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - else - { - let goalAccess = m.getLandAccess(gameState, target); - let queued = m.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 == "hunter") - { - let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); - if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) - { - if (!this.startHunting(gameState)) - { - // nothing to hunt around. Try another region if any - let nowhereToHunt = true; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let basePos = base.anchor.position(); - if (this.startHunting(gameState, basePos)) - { - ent.setMetadata(PlayerID, "base", base.ID); - 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" || unitAIStateOrder == "RETURNRESOURCE") - { - // we may have drifted towards ennemy territory during the hunt, if yes go home - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startHunting(gameState); - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - // Check that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, ent); - } - } - } - } - else if (subrole == "fisher") - { - if (ent.isIdle()) - this.startFishing(gameState); - else // if we have drifted towards ennemy territory during the fishing, go home - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startFishing(gameState); - } - } -}; - -m.Worker.prototype.retryWorking = function(gameState, subrole) -{ - switch (subrole) - { - case "gatherer": - return this.startGathering(gameState); - case "hunter": - return this.startHunting(gameState); - case "fisher": - return this.startFishing(gameState); - case "builder": - return this.startBuilding(gameState); - default: - return false; - } -}; - -m.Worker.prototype.startBuilding = function(gameState) -{ - let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) - return false; - if (m.getLandAccess(gameState, target) != this.entAccess) - return false; - this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses - return true; -}; - -m.Worker.prototype.startGathering = function(gameState) -{ - // First look for possible treasure if any - if (m.gatherTreasure(gameState, this.ent)) - return true; - - let resource = this.ent.getMetadata(PlayerID, "gather-type"); - - // If we are gathering food, try to hunt first - if (resource == "food" && this.startHunting(gameState)) - return true; - - let findSupply = function(ent, supplies) { - let ret = false; - let gatherRates = ent.resourceGatherRates(); - for (let i = 0; i < supplies.length; ++i) - { - // exhausted resource, remove it from this list - if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) - { - supplies.splice(i--, 1); - continue; - } - if (m.IsSupplyFull(gameState, supplies[i].ent)) - continue; - let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - let supplyType = supplies[i].ent.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); - if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && - supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - // not in ennemy territory - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - continue; - gameState.ai.HQ.AddTCGatherer(supplies[i].id); - ent.setMetadata(PlayerID, "supply", supplies[i].id); - ret = supplies[i].ent; - break; - } - return ret; - }; - - let navalManager = gameState.ai.HQ.navalManager; - let supply; - - // first look in our own base if accessible from our present position - if (this.baseAccess == this.entAccess) - { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.gather(supply); - return true; - } - // --> for food, try to gather from fields if any, otherwise build one if any - if (resource == "food") - { - supply = this.gatherNearestField(gameState, this.baseID); - if (supply) - { - this.ent.gather(supply); - return true; - } - supply = this.buildAnyField(gameState, this.baseID); - if (supply) - { - this.ent.repair(supply); - return true; - } - } - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - // So if we're here we have checked our whole base for a proper resource (or it was not accessible) - // --> check other bases directly accessible - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any - { - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - - // Okay may-be we haven't found any appropriate dropsite anywhere. - // Try to help building one if any accessible foundation available - let foundations = gameState.getOwnFoundations().toEntityArray(); - let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { - if (!foundation || m.getLandAccess(gameState, foundation) != this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - if (foundation.getMetadata(PlayerID, "base") != this.baseID) - this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); - this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); - this.ent.setMetadata(PlayerID, "subrole", "builder"); - this.ent.repair(foundation); - return true; - } - return false; - }, this); - if (shouldBuild) - return true; - - // Still nothing ... try bases which need a transport - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 (let 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 || m.getLandAccess(gameState, foundation) == this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - let foundationAccess = m.getLandAccess(gameState, foundation); - if (navalManager.requireTransport(gameState, this.ent, 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", "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.ent, this.base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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) - API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -/** - * if position is given, we only check if we could hunt from this position but do nothing - * otherwise the position of the entity is taken, and if something is found, we directly start the hunt - */ -m.Worker.prototype.startHunting = function(gameState, position) -{ - // First look for possible treasure if any - if (!position && m.gatherTreasure(gameState, this.ent)) - return true; - - let resources = gameState.getHuntableSupplies(); - if (!resources.hasEntities()) - return false; - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let isCavalry = this.ent.hasClass("Cavalry"); - let isRanged = this.ent.hasClass("Ranged"); - let entPosition = position ? position : this.ent.position(); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - - let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) - { - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (supplyAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) - return true; - } - return false; - }; - - let gatherRates = this.ent.resourceGatherRates(); - for (let supply of resources.values()) - { - if (!supply.position()) - continue; - - let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - if (m.IsSupplyFull(gameState, supply)) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - - let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; - // Only cavalry and range units should hunt fleeing animals - if (canFlee && !isCavalry && !isRanged) - continue; - - let supplyAccess = m.getLandAccess(gameState, supply); - if (supplyAccess != this.entAccess) - continue; - - // measure the distance to the resource - let dist = API3.SquareVectorDistance(entPosition, supply.position()); - if (dist > nearestSupplyDist) - continue; - - // Only cavalry should hunt faraway - if (!isCavalry && dist > 25000) - continue; - - // Avoid ennemy territory - let 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 cavalry should hunt far from dropsite (specially for non domestic animals which flee) - if (!isCavalry && canFlee && territoryOwner == 0) - continue; - let distanceSquare = isCavalry ? 35000 : (canFlee ? 7000 : 12000); - if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) - continue; - - nearestSupplyDist = dist; - nearestSupply = supply; - } - - if (nearestSupply) - { - if (position) - return true; - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - return false; -}; - -m.Worker.prototype.startFishing = function(gameState) -{ - if (!this.ent.position()) - return false; - - let resources = gameState.getFishableSupplies(); - if (!resources.hasEntities()) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState); - this.ent.destroy(); - return false; - } - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let fisherSea = m.getSeaAccess(gameState, this.ent); - let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")). - filter(API3.Filters.byClass("Dock")).toEntityArray(); - - let nearestDropsiteDist = function(supply) { - let distMin = 1000000; - let pos = supply.position(); - for (let dropsite of fishDropsites) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (fisherSea != m.getSeaAccess(gameState, dropsite)) - continue; - distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); - } - return distMin; - }; - - let exhausted = true; - let gatherRates = this.ent.resourceGatherRates(); - resources.forEach(function(supply) - { - if (!supply.position()) - return; - - // check that it is accessible - if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) - return; - - exhausted = false; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - return; - - if (m.IsSupplyFull(gameState, supply)) - return; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - return; - - // Avoid ennemy territory - if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) - return; - - // measure the distance from the resource to the nearest dropsite - let dist = nearestDropsiteDist(supply); - if (dist > nearestSupplyDist) - return; - - nearestSupplyDist = dist; - nearestSupply = supply; - }); - - if (exhausted) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); - this.ent.destroy(); - return false; - } - - if (nearestSupply) - { - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - if (this.ent.getMetadata(PlayerID, "subrole") == "fisher") - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -m.Worker.prototype.gatherNearestField = function(gameState, baseID) -{ - let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); - let bestFarm; - - let gatherRates = this.ent.resourceGatherRates(); - for (let field of ownFields.values()) - { - if (m.IsSupplyFull(gameState, field)) - continue; - let supplyType = field.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - let rate = 1; - let diminishing = field.getDiminishingReturns(); - if (diminishing < 1) - { - let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); - if (num > 0) - rate = Math.pow(diminishing, num); - } - // Add a penalty distance depending on rate - let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; - if (!bestFarm || dist < bestFarm.dist) - bestFarm = { "ent": field, "dist": dist, "rate": rate }; - } - // If other field foundations available, better build them when rate becomes too small - if (!bestFarm || bestFarm.rate < 0.70 && - gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) - return false; - gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); - this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); - return bestFarm.ent; -}; - -/** - * WARNING with the present options of AI orders, the unit will not gather after building the farm. - * This is done by calling the gatherNearestField function when construction is completed. - */ -m.Worker.prototype.buildAnyField = function(gameState, baseID) -{ - if (!this.ent.isBuilder()) - return false; - let bestFarmEnt = false; - let bestFarmDist = 10000000; - let pos = this.ent.position(); - for (let found of gameState.getOwnFoundations().values()) - { - if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) - continue; - let current = found.getBuildersNb(); - if (current === undefined || - current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) - continue; - let dist = API3.SquareVectorDistance(found.position(), pos); - if (dist > bestFarmDist) - continue; - bestFarmEnt = found; - bestFarmDist = dist; - } - return bestFarmEnt; -}; - -/** - * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. - * For the time being, we move towards the nearest gatherer (providing him a dropsite). - * BaseManager does also use that function to deal with its mobile dropsites. - */ -m.Worker.prototype.moveToGatherer = function(gameState, ent, forced) -{ - let pos = ent.position(); - if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) - return; - let gatherers = this.base.workersBySubrole(gameState, "gatherer"); - let dist = Math.min(); - let destination; - let access = m.getLandAccess(gameState, ent); - let types = ent.resourceDropsiteTypes(); - for (let gatherer of gatherers.values()) - { - let gathererType = gatherer.getMetadata(PlayerID, "gather-type"); - if (!gathererType || types.indexOf(gathererType) == -1) - continue; - if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || - m.getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) - continue; - let distance = API3.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). - */ -m.Worker.prototype.isInaccessibleSupply = function(gameState) -{ - if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) - return false; - let targetId = this.ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (!target) - return true; - - if (!target.resourceSupplyType()) - return false; - - let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); - let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; - if (!approachingTarget || approachingTarget != targetId) - { - this.ent.setMetadata(PlayerID, "approachingTarget", targetId); - this.ent.setMetadata(PlayerID, "approachingTime", undefined); - this.ent.setMetadata(PlayerID, "approachingPos", undefined); - this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); - let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); - if (alreadyTried && alreadyTried != targetId) - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - } - - let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); - if (carriedBefore != carriedAmount) - { - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - if (target.getMetadata(PlayerID, "inaccessibleTime")) - target.setMetadata(PlayerID, "inaccessibleTime", 0); - return false; - } - - let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - return true; - - let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); - if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) - { - let presentPos = this.ent.position(); - let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); - if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) - { - this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); - this.ent.setMetadata(PlayerID, "approachingPos", presentPos); - return false; - } - if (gameState.ai.elapsedTime - approachingTime > 10) - { - if (this.ent.getMetadata(PlayerID, "alreadyTried")) - { - target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); - return true; - } - // let's try again to reach it - this.ent.setMetadata(PlayerID, "alreadyTried", targetId); - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.gather(target); - return false; - } - } - return false; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/_petrabot.js b/install/petraBased/petra-single-base/_petrabot.js deleted file mode 100644 index 3e5d861..0000000 --- a/install/petraBased/petra-single-base/_petrabot.js +++ /dev/null @@ -1,172 +0,0 @@ -Engine.IncludeModule("common-api"); - -var PETRA = (function() { -var m = {}; - -m.maxBaseCount = 1; - -m.PetraBot = function PetraBot(settings) -{ - API3.BaseAI.call(this, settings); - - 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 m.Config(settings.difficulty, settings.behavior); - - this.savedEvents = {}; -}; - -m.PetraBot.prototype = new API3.BaseAI(); - -m.PetraBot.prototype.CustomInit = function(gameState) -{ - if (this.isDeserialized) - { - // WARNING: the deserializations should not modify the metadatas infos inside their init functions - this.turn = this.data.turn; - this.playedTurn = this.data.playedTurn; - this.elapsedTime = this.data.elapsedTime; - this.savedEvents = this.data.savedEvents; - for (let key in this.savedEvents) - { - for (let i in this.savedEvents[key]) - { - if (!this.savedEvents[key][i].entityObj) - continue; - let evt = this.savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - { - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = new API3.Entity(gameState.sharedScript, evt.entityObj); - this.savedEvents[key][i] = evtmod; - } - } - } - - this.Config.Deserialize(this.data.config); - - this.queueManager = new m.QueueManager(this.Config, {}); - this.queueManager.Deserialize(gameState, this.data.queueManager); - this.queues = this.queueManager.queues; - - this.HQ = new m.HQ(this.Config); - 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 (let i in this.Config.priorities) - this.queues[i] = new m.Queue(); - - this.queueManager = new m.QueueManager(this.Config, this.queues); - - this.HQ = new m.HQ(this.Config); - - this.HQ.init(gameState, this.queues); - - // Analyze our starting position and set a strategy - this.HQ.gameAnalysis(gameState); - } -}; - -m.PetraBot.prototype.OnUpdate = function(sharedScript) -{ - if (this.gameFinished) - return; - - for (let i in this.events) - { - if (i == "AIMetadata") // not used inside petra - continue; - if(this.savedEvents[i] !== undefined) - this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]); - else - this.savedEvents[i] = this.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.gameState.getOwnEntities().length === 0) - { - Engine.ProfileStop(); - return; // With no entities to control the AI cannot do anything - } - - this.HQ.update(this.gameState, this.queues, this.savedEvents); - - this.queueManager.update(this.gameState); - - for (let i in this.savedEvents) - this.savedEvents[i] = []; - - Engine.ProfileStop(); - } - - this.turn++; -}; - -m.PetraBot.prototype.Serialize = function() -{ - let savedEvents = {}; - for (let key in this.savedEvents) - { - savedEvents[key] = this.savedEvents[key].slice(); - for (let i in savedEvents[key]) - { - if (!savedEvents[key][i].entityObj) - continue; - let evt = savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = evt.entityObj._entity; - savedEvents[key][i] = evtmod; - } - } - - return { - "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() - }; -}; - -m.PetraBot.prototype.Deserialize = function(data, sharedScript) -{ - this.isDeserialized = true; - this.data = data; -}; - -return m; -}()); diff --git a/install/petraBased/petra-single-base/attackManager.js b/install/petraBased/petra-single-base/attackManager.js deleted file mode 100644 index ebccbee..0000000 --- a/install/petraBased/petra-single-base/attackManager.js +++ /dev/null @@ -1,805 +0,0 @@ -var PETRA = function(m) -{ - -/** Attack Manager */ - -m.AttackManager = function(Config) -{ - this.Config = Config; - - this.totalNumber = 0; - this.attackNumber = 0; - this.rushNumber = 0; - this.raidNumber = 0; - this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.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 = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.AttackManager.prototype.init = function(gameState) -{ - this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); - this.outOfPlan.registerUpdates(); -}; - -m.AttackManager.prototype.setRushes = function(allowed) -{ - if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) - { - this.maxRushes = 3; - this.rushSize = [ 16, 20, 24 ]; - } - 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 ]; - } -}; - -m.AttackManager.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.PlayerDefeated) - this.defeated[evt.playerId] = true; - - let answer = "decline"; - let other; - let targetPlayer; - for (let evt of events.AttackRequest) - { - if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) - continue; - targetPlayer = evt.player; - let available = 0; - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing") - { - if (attack.targetPlayer === targetPlayer) - available += attack.unitCollection.length; - else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) - other = attack.targetPlayer; - continue; - } - - attack.targetPlayer = targetPlayer; - - if (attack.unitCollection.length > 2) - available += attack.unitCollection.length; - } - } - - if (available > 12) // launch the attack immediately - { - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing" || - attack.targetPlayer !== targetPlayer || - attack.unitCollection.length < 3) - continue; - attack.forceStart(); - attack.requested = true; - } - } - answer = "join"; - } - else if (other !== undefined) - answer = "other"; - break; // take only the first attack request into account - } - if (targetPlayer !== undefined) - m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); - - for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks - { - for (let [targetId, unitIds] of this.bombingAttacks) - { - 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 - */ -m.AttackManager.prototype.assignBombers = function(gameState) -{ - // First some cleaning of current bombing attacks - for (let [targetId, unitIds] of this.bombingAttacks) - { - let target = gameState.getEntityById(targetId); - if (!target || !gameState.isPlayerEnemy(target.owner())) - this.bombingAttacks.delete(targetId); - else - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.owner() == PlayerID) - { - let plan = ent.getMetadata(PlayerID, "plan"); - let orders = ent.unitAIOrderData(); - let 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); - } - } - - let bombers = gameState.updatingCollection("bombers", API3.Filters.byClassesOr(["BoltShooter", "Catapult"]), gameState.getOwnUnits()); - for (let 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - continue; - } - let alreadyBombing = false; - for (let unitIds of this.bombingAttacks.values()) - { - if (!unitIds.has(ent.id())) - continue; - alreadyBombing = true; - break; - } - if (alreadyBombing) - break; - - let range = ent.attackRange("Ranged").max; - let entPos = ent.position(); - let access = m.getLandAccess(gameState, ent); - for (let struct of gameState.getEnemyStructures().values()) - { - let structPos = struct.position(); - let x; - let z; - if (struct.hasClass("Field")) - { - if (!struct.resourceSupplyNumGatherers() || - !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) - continue; - } - let dist = API3.VectorDistance(entPos, structPos); - if (dist > range) - { - let safety = struct.footprintRadius() + 30; - x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; - z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; - let 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 (let [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; - } - } -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.AttackManager.prototype.update = function(gameState, queues, events) -{ - if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) - { - this.debugTime = gameState.ai.elapsedTime; - API3.warn(" upcoming attacks ================="); - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" started attacks =================="); - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" =================================="); - } - - this.checkEvents(gameState, events); - - let unexecutedAttacks = { "Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0 }; - for (let attackType in this.upcomingAttacks) - { - for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) - { - let attack = this.upcomingAttacks[attackType][i]; - attack.checkEvents(gameState, events); - - if (attack.isStarted()) - API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); - - let updateStep = attack.updatePreparation(gameState); - // now we're gonna check if the preparation time is over - if (updateStep == 1 || attack.isPaused()) - { - // just chillin' - if (attack.state == "unexecuted") - ++unexecutedAttacks[attackType]; - } - else if (updateStep == 0) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - else if (updateStep == 2) - { - if (attack.StartAttack(gameState)) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); - if (this.Config.chat) - m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); - this.startedAttacks[attackType].push(attack); - } - else - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - } - } - - for (let attackType in this.startedAttacks) - { - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - attack.checkEvents(gameState, events); - // okay so then we'll update the attack. - if (attack.isPaused()) - continue; - let remaining = attack.update(gameState, events); - if (!remaining) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } - } - - // creating plans after updating because an aborted plan might be reused in that case. - - let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; - if (this.rushNumber < this.maxRushes && barracksNb >= 1) - { - if (unexecutedAttacks.Rush === 0) - { - // we have a barracks and we want to rush, rush. - let data = { "targetSize": this.rushSize[this.rushNumber] }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); - if (!attackPlan.failed) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Rush.push(attackPlan); - } - this.rushNumber++; - } - } - else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 && - this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) && - (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) - { - if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack - { - let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type); - if (attackPlan.failed) - this.attackPlansEncounteredWater = true; // hack - else - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[type].push(attackPlan); - } - this.attackNumber++; - } - } - - if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) - { - let target; - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - target = gameState.getEntityById(targetId); - if (!target) - 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 > 1 && gameState.ai.playedTurn % 5 == 0) - this.assignBombers(gameState); -}; - -m.AttackManager.prototype.getPlan = function(planName) -{ - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - for (let attackType in this.startedAttacks) - { - for (let attack of this.startedAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - return undefined; -}; - -m.AttackManager.prototype.pausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(false); -}; - -m.AttackManager.prototype.pauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(true); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(false); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(false); -}; - -m.AttackManager.prototype.getAttackInPreparation = function(type) -{ - 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. - */ -m.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; - - let veto = {}; - for (let i in this.defeated) - veto[i] = true; - // No rush if enemy too well defended (i.e. iberians) - if (attack.type == "Rush") - { - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (!gameState.isPlayerEnemy(i) || veto[i]) - continue; - if (this.defeated[i]) - continue; - let enemyDefense = 0; - for (let ent of gameState.getEnemyStructures(i).values()) - if (ent.hasClass("Tower") || ent.hasClass("Fortress")) - enemyDefense++; - if (enemyDefense > 6) - veto[i] = true; - } - } - - // then if not a huge attack, continue attacking our previous target as long as it has some entities, - // otherwise target the most accessible one - if (attack.type != "HugeAttack") - { - if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && - !this.defeated[this.currentEnemyPlayer] && - gameState.isPlayerEnemy(this.currentEnemyPlayer) && - gameState.getEntities(this.currentEnemyPlayer).hasEntities()) - return this.currentEnemyPlayer; - - let distmin; - let ccmin; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let ourcc of ccEnts.values()) - { - if (ourcc.owner() != PlayerID) - continue; - let ourPos = ourcc.position(); - let access = m.getLandAccess(gameState, ourcc); - for (let enemycc of ccEnts.values()) - { - if (veto[enemycc.owner()]) - continue; - if (!gameState.isPlayerEnemy(enemycc.owner())) - continue; - if (access != m.getLandAccess(gameState, enemycc)) - continue; - let dist = API3.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 (veto[i]) - continue; - if (!gameState.isPlayerEnemy(i)) - continue; - let enemyCount = 0; - let enemyCivCentre = false; - for (let 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; -}; - -/** - * Target the player with the most advanced wonder. - * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. - */ -m.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let enemyWonder; - let moreAdvanced; - for (let wonder of gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")).values()) - { - if (wonder.owner() == 0) - continue; - let progress = wonder.foundationProgress(); - if (progress === undefined) - { - enemyWonder = wonder; - break; - } - if (enemyWonder && moreAdvanced > progress) - continue; - enemyWonder = wonder; - moreAdvanced = progress; - } - if (enemyWonder) - { - enemyPlayer = enemyWonder.owner(); - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - } - return enemyPlayer; -}; - -/** - * Target the player with the most relics (including gaia). - */ -m.AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let allRelics = gameState.updatingGlobalCollection("allRelics", API3.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; - - let 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. */ -m.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - if (attack.targetPlayer === player) - attack.targetPlayer = undefined; - - for (let attackType in this.startedAttacks) - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - if (attack.targetPlayer === player) - { - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } -}; - -m.AttackManager.prototype.raidTargetEntity = function(gameState, ent) -{ - let data = { "target": ent }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); - if (attackPlan.failed) - return null; - if (this.Config.debug > 1) - API3.warn("Military Manager: Raiding plan " + this.totalNumber); - this.raidNumber++; - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Raid.push(attackPlan); - return attackPlan; -}; - -/** - * Return the number of units from any of our attacking armies around this position - */ -m.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) -{ - let num = 0; - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - { - if (!attack.position) // this attack may be inside a transport - continue; - if (API3.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 - */ -m.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) -{ - if (!target || !target.position()) - return false; - if (!data.range && !data.armyID) - { - API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); - return false; - } - let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; - let pos = target.position(); - let attackType = "Attack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); - if (attackPlan.failed) - return false; - this.totalNumber++; - attackPlan.init(gameState); - this.startedAttacks[attackType].push(attackPlan); - - let targetAccess = m.getLandAccess(gameState, target); - for (let army of gameState.ai.HQ.defenseManager.armies) - { - if (data.range) - { - army.recalculatePosition(gameState); - if (API3.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) - { - let unitId = army.ownEntities[0]; - army.removeOwn(gameState, unitId); - let unit = gameState.getEntityById(unitId); - let accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || - unit.position() && m.getLandAccess(gameState, unit) == targetAccess; - if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) - { - unit.setMetadata(PlayerID, "plan", attackPlan.name); - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.unitCollection.updateEnt(unit); - } - } - } - if (!attackPlan.unitCollection.hasEntities()) - { - attackPlan.Abort(gameState); - return false; - } - for (let unit of attackPlan.unitCollection.values()) - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.targetPlayer = target.owner(); - attackPlan.targetPos = pos; - attackPlan.target = target; - attackPlan.state = "arrived"; - return true; -}; - -m.AttackManager.prototype.Serialize = function() -{ - let properties = { - "totalNumber": this.totalNumber, - "attackNumber": this.attackNumber, - "rushNumber": this.rushNumber, - "raidNumber": this.raidNumber, - "debugTime": this.debugTime, - "maxRushes": this.maxRushes, - "rushSize": this.rushSize, - "currentEnemyPlayer": this.currentEnemyPlayer, - "defeated": this.defeated - }; - - let upcomingAttacks = {}; - for (let key in this.upcomingAttacks) - { - upcomingAttacks[key] = []; - for (let attack of this.upcomingAttacks[key]) - upcomingAttacks[key].push(attack.Serialize()); - } - - let startedAttacks = {}; - for (let key in this.startedAttacks) - { - startedAttacks[key] = []; - for (let attack of this.startedAttacks[key]) - startedAttacks[key].push(attack.Serialize()); - } - - return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; -}; - -m.AttackManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.upcomingAttacks = {}; - for (let key in data.upcomingAttacks) - { - this.upcomingAttacks[key] = []; - for (let dataAttack of data.upcomingAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.upcomingAttacks[key].push(attack); - } - } - - this.startedAttacks = {}; - for (let key in data.startedAttacks) - { - this.startedAttacks[key] = []; - for (let dataAttack of data.startedAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.startedAttacks[key].push(attack); - } - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/attackPlan.js b/install/petraBased/petra-single-base/attackPlan.js deleted file mode 100644 index f354142..0000000 --- a/install/petraBased/petra-single-base/attackPlan.js +++ /dev/null @@ -1,2171 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This is an attack plan: - * It deals with everything in an attack, from picking a target to picking a path to it - * To making sure units are built, and pushing elements to the queue manager otherwise - * It also handles the actual attack, though much work is needed on that. - */ - -m.AttackPlan = function(gameState, Config, uniqueID, type, data) -{ - this.Config = Config; - this.name = uniqueID; - this.type = type || "Attack"; - this.state = "unexecuted"; - this.forced = false; // true when this attacked has been forced to help an ally - - if (data && data.target) - { - 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.uniqueTargetId = data && data.uniqueTargetId || undefined; - - // get a starting rallyPoint ... will be improved later - let rallyPoint; - let rallyAccess; - let allAccesses = {}; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let access = m.getLandAccess(gameState, base.anchor); - if (!rallyPoint) - { - rallyPoint = base.anchor.position(); - rallyAccess = access; - } - if (!allAccesses[access]) - allAccesses[access] = base.anchor.position(); - } - if (!rallyPoint) // no base ? take the position of any of our entities - { - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position()) - continue; - let access = m.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 (let structure of gameState.getEnemyStructures().values()) - { - if (this.target && structure.id() != this.target.id()) - continue; - if (!structure.position()) - continue; - let access = m.getLandAccess(gameState, structure); - if (access in allAccesses) - { - this.overseas = 0; - this.rallyPoint = allAccesses[access]; - break; - } - else if (!this.overseas) - { - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); - if (!sea) - { - if (this.target) - { - API3.warn("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 = 70; - - // 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 == "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - if (data && data.targetSize) - this.unitStat.Infantry.targetSize = data.targetSize; - this.neededShips = 1; - } - else if (type == "Raid") - { - priority = 150; - this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [ ["strength", 1] ] }; - this.neededShips = 1; - } - else if (type == "HugeAttack") - { - 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.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], - "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - this.neededShips = 3; - } - - // Put some randomness on the attack size - let variation = randFloat(0.8, 1.2); - // and lower priority and smaller sizes for easier difficulty levels - if (this.Config.difficulty < 2) - { - priority *= 0.6; - variation *= 0.5; - } - else if (this.Config.difficulty < 3) - { - priority *= 0.8; - variation *= 0.8; - } - for (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(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 (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); - } - - // 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 = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders - - // 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; -}; - -m.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(API3.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 (let cat in this.unitStat) - { - let Unit = this.unitStat[cat]; - this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[cat].registerUpdates(); - if (this.canBuildUnits) - this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); - } -}; - -m.AttackPlan.prototype.getName = function() -{ - return this.name; -}; - -m.AttackPlan.prototype.getType = function() -{ - return this.type; -}; - -m.AttackPlan.prototype.isStarted = function() -{ - return this.state !== "unexecuted" && this.state !== "completing"; -}; - -m.AttackPlan.prototype.isPaused = function() -{ - return this.paused; -}; - -m.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. - */ -m.AttackPlan.prototype.canStart = function() -{ - if (!this.canBuildUnits) - return true; - - for (let unitCat in this.unitStat) - if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) - return false; - - return true; -}; - -m.AttackPlan.prototype.mustStart = function() -{ - if (this.isPaused()) - return false; - - if (!this.canBuildUnits) - return this.unitCollection.hasEntities(); - - let MaxReachedEverywhere = true; - let MinReachedEverywhere = true; - for (let unitCat in this.unitStat) - { - let 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 == "Raid" && this.target && this.target.foundationProgress() && - this.target.foundationProgress() > 50; - return false; -}; - -m.AttackPlan.prototype.forceStart = function() -{ - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - Unit.targetSize = 0; - Unit.minSize = 0; - } - this.forced = true; -}; - -/** Adds a build order. If resetQueue is true, this will reset the queue. */ -m.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; - let Unit = this.unitStat[name]; - this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[name].registerUpdates(); - this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); - if (resetQueue) - { - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - } -}; - -m.AttackPlan.prototype.addSiegeUnits = function(gameState) -{ - if (this.siegeState == 2 || this.state !== "unexecuted") - return false; - - let civ = gameState.getPlayerCiv(); - let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; - let hasTrainer = [false, false, false]; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - let trainables = ent.trainableEntities(civ); - if (!trainables) - continue; - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.available(gameState)) - continue; - for (let i = 0; i < classes.length; ++i) - if (classes[i].every(c => template.hasClass(c))) - hasTrainer[i] = true; - } - } - if (hasTrainer.every(e => !e)) - return false; - let i = this.name % classes.length; - for (let k = 0; k < classes.length; ++k) - { - if (hasTrainer[i]) - break; - i = ++i % classes.length; - } - - this.siegeState = 2; - let targetSize; - if (this.Config.difficulty < 3) - targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); - else - targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; - targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); - if (!targetSize) - return true; - // no minsize as we don't want the plan to fail at the last minute though. - let 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". */ -m.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 == "completing") - { - // if our target was destroyed, go back to "unexecuted" state - if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) - { - this.state = "unexecuted"; - this.target = undefined; - } - else - { - // check that all units have finished with their transport if needed - if (this.waitingForTransport()) - return 1; - // bloqued units which cannot finish their order should not stop the attack - if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) - return 1; - return 2; - } - } - - 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 1; - - if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) - this.assignUnits(gameState); - if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) - this.reassignCavUnit(gameState); // reassign some cav (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.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - else // Abort the plan so that its units will be reassigned to other plans. - { - if (this.Config.debug > 1) - { - let am = gameState.ai.HQ.attackManager; - API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + - " rush " + am.upcomingAttacks.Rush.length + - " attack " + am.upcomingAttacks.Attack.length + - " huge " + am.upcomingAttacks.HugeAttack.length); - API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + - " rush " + am.startedAttacks.Rush.length + - " attack " + am.startedAttacks.Attack.length + - " huge " + am.startedAttacks.HugeAttack.length); - } - return 0; - } - } - else if (this.mustStart()) - { - if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) - { - // keep on while the units finish being trained, then we'll start - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - return 1; - } - } - else - { - if (this.canBuildUnits) - { - // We still have time left to recruit units and do stuffs. - if (this.siegeState == 0 || this.siegeState == 1 && 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 0; // will abort the plan - } - return 1; - } - - // if we're here, it means we must start - this.state = "completing"; - - // Raids have their predefined target - if (!this.target && !this.chooseTarget(gameState)) - return 0; - if (!this.overseas) - this.getPathToTarget(gameState); - - if (this.type == "Raid") - this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; - else - { - if (this.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 (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) - continue; - unitIds.delete(entId); - ent.stopMoving(); - } - } - - let rallyPoint = this.rallyPoint; - let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); - for (let 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", "attack"); - ent.setMetadata(PlayerID, "subrole", "completing"); - let queued = false; - if (ent.resourceCarrying() && ent.resourceCarrying().length) - queued = m.returnResources(gameState, ent); - let index = m.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 - let plan = this.name; - gameState.ai.queueManager.removeQueue("plan_" + plan); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); - return 1; -}; - - -m.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 (let order of this.buildOrders) - { - let 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; - return va - vb; - }); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - { - API3.warn("===================================="); - API3.warn("======== build order for plan " + this.name); - for (let order of this.buildOrders) - { - let specialData = "Plan_"+this.name+"_"+order[4]; - let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); - let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); - let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); - let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); - API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + - " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); - } - API3.warn("===================================="); - } - - let firstOrder = this.buildOrders[0]; - if (firstOrder[0] < firstOrder[3].targetSize) - { - // find the actual queue we want - let queue = this.queue; - if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && - firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) - 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) - { - let 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) - API3.warn("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) - API3.warn("attack template " + template + " added for plan " + this.name); - let max = firstOrder[3].batchSize; - let specialData = "Plan_" + this.name + "_" + firstOrder[4]; - let data = { "plan": this.name, "special": specialData, "base": 0 }; - data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; - let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); - if (trainingPlan.template) - queue.addPlan(trainingPlan); - else if (this.Config.debug > 1) - API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + - " build3 " + uneval(firstOrder[3].interests)); - } - } - } -}; - -m.AttackPlan.prototype.assignUnits = function(gameState) -{ - let 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 (let 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 == "Raid") - { - // Raid are fast cavalry attack: assign all cav except some for hunting - let num = 0; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Cavalry") || !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 (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) - { - if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) - continue; - if (ent.hasClass("Ship") || ent.hasClass("Support") || 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 (let 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, - // 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 - let num = 0; - let numbase = {}; - let keep = this.type != "Rush" ? - 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; - keep = Math.round(this.Config.popScaling * keep); - for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) - { - if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) - continue; - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID) - numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; - else - { - API3.warn("Petra problem ent without base "); - m.dumpEntity(ent); - continue; - } - if (num++ < keep || numbase[baseID] < 5) - continue; - if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; -}; - -m.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) Cav unit to fasten raid preparation. */ -m.AttackPlan.prototype.reassignCavUnit = function(gameState) -{ - for (let ent of this.unitCollection.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) - continue; - let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); - ent.setMetadata(PlayerID, "plan", raid.name); - this.unitCollection.updateEnt(ent); - raid.unitCollection.updateEnt(ent); - return; - } -}; - -m.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) - let targetIndex = m.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 (let base of gameState.ai.HQ.baseManagers) - { - let anchor = base.anchor; - if (!anchor || !anchor.position()) - continue; - let dist = API3.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; - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); - if (sea) - { - this.overseas = sea; - gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); - } - else - { - API3.warn("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 - */ -m.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 API3.EntityCollection(gameState.sharedScript); - let ent = gameState.getEntityById(this.uniqueTargetId); - if (ent) - targets.addEnt(ent); - } - else - { - if (this.type == "Raid") - targets = this.raidTargetFinder(gameState); - else if (this.type == "Rush" || this.type == "Attack") - { - 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 (let 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 = API3.SquareVectorDistance(ent.position(), position); - // In normal attacks, disfavor fields - if (this.type != "Rush" && this.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 - */ -m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - if (gameState.getVictoryConditions().has("wonder")) - for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("regicide")) - for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("capture_the_relic")) - for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) - targets.addEnt(ent); - targets = targets.filter(this.isValidTarget, this); - if (targets.hasEntities()) - return targets; - - let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); - targets = validTargets.filter(API3.Filters.byClass("CivCentre")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); - // If there's nothing, attack anything else that's less critical - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("Town")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.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(API3.Filters.byClass("ConquestCritical")). - filter(API3.Filters.not(API3.Filters.byClass("Ship"))); - return targets; -}; - -m.AttackPlan.prototype.isValidTarget = function(ent) -{ - if (!ent.position()) - return false; - if (this.sameLand && m.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 */ -m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.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 (let building of buildings) - { - if (building.owner() == 0) - continue; - if (building.hasDefensiveFire()) - continue; - if (!this.isValidTarget(building)) - continue; - let pos = building.position(); - let defended = false; - for (let defense of buildings) - { - if (!defense.hasDefensiveFire()) - continue; - let dist = API3.SquareVectorDistance(pos, defense.position()); - if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 - { - defended = true; - break; - } - } - if (defended) - continue; - let dist = API3.SquareVectorDistance(pos, this.position); - if (dist > minDist) - continue; - minDist = dist; - target = building; - } - if (target) - targets.addEnt(target); - - if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) - targets = this.rushTargetFinder(gameState); - - return targets; -}; - -/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ -m.AttackPlan.prototype.raidTargetFinder = function(gameState) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - let 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 - */ -m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) -{ - if (m.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) - return target; - - let targetPos = target.position(); - let startPos = { "x": position[0], "y": position[1] }; - let endPos = { "x": targetPos[0], "y": targetPos[1] }; - let blocker; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); - if (!path.length) - return undefined; - - let pathPos = [path[0].x, path[0].y]; - let dist = API3.VectorDistance(pathPos, targetPos); - let radius = target.obstructionRadius().max; - for (let 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("Gates")) - 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. - let structPos = struct.position(); - let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; - let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; - - if (struct.get("Obstruction/Static")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Static/@width"); - let depth = +struct.get("Obstruction/Static/@depth"); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - } - else if (struct.get("Obstruction/Obstructions")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Obstructions/Door/@width"); - let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); - let 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")); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - // check that the path does not cross this gate (could happen if not locked) - for (let i = 1; i < path.length; ++i) - { - let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; - let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; - let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; - let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; - if (v1 * v2 < 0) - { - let 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")) - { - let r = +this.get("Obstruction/Unit/@radius"); - if (x*x + y*y < r*r) - { - blocker = struct; - break; - } - } - } - - if (blocker && blocker.hasClass("StoneWall")) - { -/* if (this.hasSiegeUnits()) - { */ - this.isBlocked = true; - return blocker; -/* } - return undefined; */ - } - else if (blocker) - { - this.isBlocked = true; - return blocker; - } - - return target; -}; - -m.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) -{ - let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let endAccess = m.getLandAccess(gameState, this.target); - if (startAccess != endAccess) - return false; - - Engine.ProfileStart("AI Compute path"); - let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; - let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); - this.path = []; - this.path.push(this.targetPos); - for (let p in path) - this.path.push([path[p].x, path[p].y]); - this.path.push(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 */ -m.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. - */ -m.AttackPlan.prototype.StartAttack = function(gameState) -{ - if (this.Config.debug > 1) - API3.warn("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. - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); - - for (let ent of this.unitCollection.values()) - { - ent.setMetadata(PlayerID, "subrole", "walking"); - let stance = ent.isPackable() ? "standground" : "aggressive"; - if (ent.getStance() != stance) - ent.setStance(stance); - } - - let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let targetAccess = m.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 (let 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 */ -m.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.state == "walking" && !this.UpdateWalking(gameState, events)) - { - Engine.ProfileStop(); - return 0; - } - - if (this.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", "attacking"); - }); - if (this.type == "Rush") // try to find a better target for rush - { - let 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; - } - - let time = gameState.ai.elapsedTime; - let attackedByStructure = {}; - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - let ourUnit = gameState.getEntityById(evt.target); - if (!ourUnit || !attacker || !attacker.position()) - continue; - if (!attacker.hasClass("Unit")) - { - attackedByStructure[evt.target] = true; - continue; - } - if (m.isSiegeUnit(ourUnit)) - { // if our siege units are attacked, we'll send some units to deal with enemies. - let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - // And if this attacker is a non-ranged siege unit and our unit also, attack it - if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) - { - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) - { - // 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 (m.isSiegeUnit(attacker)) - { // if our unit is attacked by a siege unit, we'll send some melee units to help it. - let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - // Look first for nearby units to help us if possible - let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) - continue; - let orderData = ent.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - continue; - } - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - 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 - let orderData = ourUnit.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - { - if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) - continue; - } - } - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - } - - let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); - let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); - - // Count the number of times an enemy is targeted, to prevent all units to follow the same target - let unitTargets = {}; - for (let ent of this.unitCollection.values()) - { - if (ent.hasClass("Ship")) // TODO What to do with ships - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || target.hasClass("Structure")) - continue; - if (!(targetId in unitTargets)) - { - if (m.isSiegeUnit(target) || target.hasClass("Hero")) - unitTargets[targetId] = -8; - else if (target.hasClass("Champion") || target.hasClass("Ship")) - unitTargets[targetId] = -5; - else - unitTargets[targetId] = -3; - } - ++unitTargets[targetId]; - } - let veto = {}; - for (let target in unitTargets) - if (unitTargets[target] > 0) - veto[target] = true; - - let targetClassesUnit; - let targetClassesSiege; - if (this.type == "Rush") - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto }; - else - { - if (this.target.hasClass("Fortress")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto }; - else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; - else - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "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", "Blacksmith"); - targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); - } - - 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 - let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; - for (let check = 0; check < lgth; check++) - { - let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); - if (!ent || !ent.position()) - continue; - // Do not reaffect units which have reacted to an attack in that same turn - if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) - continue; - - let targetId; - let 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; - let siegeUnit = m.isSiegeUnit(ent); - if (ent.isIdle()) - needsUpdate = true; - else if (siegeUnit && targetId) - { - let 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) - { - let 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("Cavalry") && !ent.hasClass("Ranged") && - target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") - maybeUpdate = true; - } - - // don't update too soon if not necessary - if (!needsUpdate) - { - if (!maybeUpdate) - continue; - let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; - let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); - if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) - continue; - } - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - let range = 60; - let 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("Cavalry")) - range += 30; - range = range * range; - let entAccess = m.getLandAccess(gameState, ent); - // Checking for gates if we're a siege unit. - if (siegeUnit) - { - let mStruct = enemyStructures.filter(enemy => { - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (enemy.foundationProgress() == 0) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasDefensiveFire()) - vala += 1000; - else if (structa.hasClass("ConquestCritical")) - vala += 200; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasDefensiveFire()) - valb += 1000; - else if (structb.hasClass("ConquestCritical")) - valb += 200; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); - } - } - else - { - if (!ent.hasClass("Ranged")) - { - let 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 - { - let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); - let mUnit = enemyUnits.filter(enemy => { - if (!enemy.position()) - return false; - if (enemy.hasClass("Animal")) - return false; - if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") - return false; - let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); - if (dist > range) - return false; - if (m.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()] && API3.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.countersClasses(unitA.classes())) - vala += 100; - let valb = unitB.hasClass("Support") ? 50 : 0; - if (ent.countersClasses(unitB.classes())) - valb += 100; - let distA = unitA.getMetadata(PlayerID, "distance"); - let 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; - }); - let rand = randIntExclusive(0, mUnit.length * 0.1); - ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); - } - else if (this.isBlocked) - ent.attack(this.target.id(), false); - else if (API3.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.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; - else - targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; - } - else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); - } - else - { - let mStruct = enemyStructures.filter(enemy => { - if (this.isBlocked && enemy.id() != this.target.id()) - return false; - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }, this).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasClass("ConquestCritical")) - vala += 100; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasClass("ConquestCritical")) - valb += 100; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), false); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.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; - if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) - return; - let dist = API3.SquareVectorDistance(unit.position(), ent.position()); - if (dist > distmin) - return; - distmin = dist; - attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); - }); - if (attacker) - ent.attack(attacker.id(), m.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.lastPosition = this.position; - Engine.ProfileStop(); - - return this.unitCollection.length; -}; - -m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) -{ - let done = true; - for (let 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; - } - - if (done) - { - this.state = "arrived"; - return; - } - - // if we are attacked while waiting the rest of the army, retaliate - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - if (!attacker || !gameState.getEntityById(evt.target)) - continue; - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.isIdle()) - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - } - break; - } -}; - -m.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 (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let 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 = "arrived"; - return true; - } - } - - // basically haven't moved an inch: very likely stuck) - if (API3.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 (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) - { - let dist = API3.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 = this.position; - - if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) - { - if (!this.path[0][0] || !this.path[0][1]) - API3.warn("Start: Problem with path " + uneval(this.path)); - // We're stuck, presumably. Check if there are no walls just close to us. - for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) - { - if (API3.SquareVectorDistance(this.position, ent.position()) > 800) - continue; - let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; - // there are walls, so check if we can attack - if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); - this.state = "arrived"; - return true; - } - // abort plan - if (this.Config.debug > 1) - API3.warn("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 (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) - { - 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) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - } - - return true; -}; - -m.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) - { - let holder = m.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) - API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); - let accessIndex = this.getAttackAccess(gameState); - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.uniqueTargetId) - return false; - - // Check if we could help any current attack - let attackManager = gameState.ai.HQ.attackManager; - for (let attackType in attackManager.startedAttacks) - { - for (let 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 != m.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) - { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer !== undefined) - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.Config.debug > 1) - API3.warn("No new target found. Remaining units " + this.unitCollection.length); - return false; - } - } - if (this.Config.debug > 1) - API3.warn("We will help one of our other attacks"); - } - this.targetPos = this.target.position(); - } - return true; -}; - -/** reset any units */ -m.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()) - { - let access = this.getAttackAccess(gameState); - let dist = Math.min(); - if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) - { - rallyPoint = this.rallyPoint; - dist = API3.SquareVectorDistance(this.position, rallyPoint); - } - // Then check if we have a nearer base (in case this attack has captured one) - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); - if (newdist > dist) - continue; - dist = newdist; - rallyPoint = base.anchor.position(); - } - } - - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - ent.stopMoving(); - if (rallyPoint) - ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); - this.removeUnit(ent); - } - } - - for (let unitCat in this.unitStat) - this.unit[unitCat].unregister(); - - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); -}; - -m.AttackPlan.prototype.removeUnit = function(ent, update) -{ - if (ent.getMetadata(PlayerID, "role") == "attack") - { - if (ent.hasClass("CitizenSoldier")) - ent.setMetadata(PlayerID, "role", "worker"); - else - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - } - ent.setMetadata(PlayerID, "plan", -1); - if (update) - this.unitCollection.updateEnt(ent); -}; - -m.AttackPlan.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.EntityRenamed) - { - if (!this.target || this.target.id() != evt.entity) - continue; - if (this.type == "Raid" && !this.isStarted()) - this.target = undefined; - else - this.target = gameState.getEntityById(evt.newentity); - if (this.target) - this.targetPos = this.target.position(); - } - - for (let evt of events.OwnershipChanged) // capture event - if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) - this.target = undefined; - - for (let evt of events.PlayerDefeated) - { - if (this.targetPlayer !== evt.playerId) - continue; - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - this.target = undefined; - } - - if (!this.overseas || this.state !== "unexecuted") - return; - // let's check if an enemy has built a structure at our access - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.position() || !ent.hasClass("Structure")) - continue; - if (!gameState.isPlayerEnemy(ent.owner())) - continue; - let access = m.getLandAccess(gameState, ent); - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - this.overseas = 0; - this.rallyPoint = base.anchor.position(); - } - } -}; - -m.AttackPlan.prototype.waitingForTransport = function() -{ - for (let ent of this.unitCollection.values()) - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return true; - return false; -}; - -m.AttackPlan.prototype.hasSiegeUnits = function() -{ - for (let ent of this.unitCollection.values()) - if (m.isSiegeUnit(ent)) - return true; - return false; -}; - -m.AttackPlan.prototype.hasForceOrder = function(data, value) -{ - for (let ent of this.unitCollection.values()) - { - if (data && +ent.getMetadata(PlayerID, data) !== value) - continue; - let orders = ent.unitAIOrderData(); - for (let order of orders) - if (order.force) - return true; - } - 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. - */ -m.AttackPlan.prototype.getAttackAccess = function(gameState) -{ - for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) - return m.getLandAccess(gameState, ent); - - return 0; -}; - -m.AttackPlan.prototype.debugAttack = function() -{ - API3.warn("---------- attack " + this.name); - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); - } - API3.warn("------------------------------"); -}; - -m.AttackPlan.prototype.Serialize = function() -{ - let properties = { - "name": this.name, - "type": this.type, - "state": this.state, - "forced": this.forced, - "rallyPoint": 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, - "isBlocked": this.isBlocked, - "targetPlayer": this.targetPlayer, - "target": this.target !== undefined ? this.target.id() : undefined, - "targetPos": this.targetPos, - "uniqueTargetId": this.uniqueTargetId, - "path": this.path - }; - - return { "properties": properties }; -}; - -m.AttackPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - if (this.target) - this.target = gameState.getEntityById(this.target); - - this.failed = undefined; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/baseManager.js b/install/petraBased/petra-single-base/baseManager.js deleted file mode 100644 index b2260f6..0000000 --- a/install/petraBased/petra-single-base/baseManager.js +++ /dev/null @@ -1,1111 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Base Manager - * Handles lower level economic stuffs. - * Some tasks: - * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. - * -giving feedback/estimates on GR - * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. - * -getting good spots for dropsites - * -managing dropsite use in the base - * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) - */ - -m.BaseManager = function(gameState, Config) -{ - this.Config = Config; - this.ID = gameState.ai.uniqueIDs.bases++; - - // anchor building: seen as the main building of the base. Needs to have territorial influence - this.anchor = undefined; - this.anchorId = undefined; - this.accessIndex = undefined; - - // Maximum distance (from any dropsite) to look for resources - // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max - this.maxDistResourceSquare = 360*360; - - this.constructing = false; - // Defenders to train in this cc when its construction is finished - this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; - - // vector for iterating, to check one use the HQ map. - this.territoryIndices = []; - - this.timeNextIdleCheck = 0; -}; - -m.BaseManager.prototype.init = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else if (state != "captured") - this.neededDefenders = 0; - this.workerObject = new m.Worker(this); - // entitycollections - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); - this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.mobileDropsites = this.units.filter(API3.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 (let res of Resources.GetCodes()) - { - this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; - this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; - } -}; - -m.BaseManager.prototype.reset = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else - this.constructing = false; - - if (state != "captured" || this.Config.difficulty < 3) - this.neededDefenders = 0; - else - this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); -}; - -m.BaseManager.prototype.assignEntity = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "base", this.ID); - this.units.updateEnt(ent); - this.workers.updateEnt(ent); - this.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) - this.assignResourceToDropsite(gameState, ent); -}; - -m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) -{ - if (!anchorEntity.hasClass("CivCentre")) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); - else - { - this.anchor = anchorEntity; - this.anchorId = anchorEntity.id(); - this.anchor.setMetadata(PlayerID, "baseAnchor", true); - gameState.ai.HQ.resetBaseCache(); - } - anchorEntity.setMetadata(PlayerID, "base", this.ID); - this.buildings.updateEnt(anchorEntity); - this.accessIndex = m.getLandAccess(gameState, anchorEntity); - return true; -}; - -/* we lost our anchor. Let's reaffect our units and buildings */ -m.BaseManager.prototype.anchorLost = function(gameState, ent) -{ - this.anchor = undefined; - this.anchorId = undefined; - this.neededDefenders = 0; - gameState.ai.HQ.resetBaseCache(); -}; - -/** Set a building of an anchorless base */ -m.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) -{ - if (!this.buildings.hasEntities()) - { - if (!m.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); - this.accessIndex = m.getLandAccess(gameState, ent); - } - else if (this.accessIndex != m.getLandAccess(gameState, ent)) - API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + - " has been assigned " + ent.templateName() + " with access" + m.getLandAccess(gameState, ent)); - - 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. - */ -m.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) -{ - if (this.dropsites[dropsite.id()]) - { - if (this.Config.debug > 0) - warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); - return; - } - - let accessIndex = this.accessIndex; - let dropsitePos = dropsite.position(); - let dropsiteId = dropsite.id(); - this.dropsites[dropsiteId] = true; - - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) - accessIndex = m.getLandAccess(gameState, dropsite); - - let maxDistResourceSquare = this.maxDistResourceSquare; - for (let type of dropsite.resourceDropsiteTypes()) - { - let resources = gameState.getResourceSupplies(type); - if (!resources.length) - continue; - - let nearby = this.dropsiteSupplies[type].nearby; - let medium = this.dropsiteSupplies[type].medium; - let faraway = this.dropsiteSupplies[type].faraway; - - resources.forEach(function(supply) - { - if (!supply.position()) - return; - if (supply.hasClass("Animal")) // moving resources are treated differently - return; - if (supply.hasClass("Field")) // fields are treated separately - return; - if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately - return; - // quick accessibility check - if (m.getLandAccess(gameState, supply) != accessIndex) - return; - - let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); - if (dist < maxDistResourceSquare) - { - if (dist < maxDistResourceSquare/16) // distmax/4 - nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else if (dist < maxDistResourceSquare/4) // distmax/2 - medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else - faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - } - }); - - nearby.sort((r1, r2) => r1.dist - r2.dist); - medium.sort((r1, r2) => r1.dist - r2.dist); - faraway.sort((r1, r2) => r1.dist - r2.dist); - -/* let debug = false; - if (debug) - { - faraway.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); - }); - medium.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); - }); - nearby.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); - }); - } */ - } - - // Allows all allies to use this dropsite except if base anchor to be sure to keep - // a minimum of resources for this base - Engine.PostCommand(PlayerID, { - "type": "set-dropsite-sharing", - "entities": [dropsiteId], - "shared": dropsiteId != this.anchorId - }); -}; - -// completely remove the dropsite resources from our list. -m.BaseManager.prototype.removeDropsite = function(gameState, ent) -{ - if (!ent.id()) - return; - - let removeSupply = function(entId, supply){ - for (let i = 0; i < supply.length; ++i) - { - // exhausted resource, remove it from this list - if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) - supply.splice(i--, 1); - // resource assigned to the removed dropsite, remove it - else if (supply[i].dropsite == entId) - supply.splice(i--, 1); - } - }; - - for (let type in this.dropsiteSupplies) - { - removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); - removeSupply(ent.id(), this.dropsiteSupplies[type].medium); - removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); - } - - this.dropsites[ent.id()] = undefined; -}; - -/** - * Returns the position of the best place to build a new dropsite for the specified resource - */ -m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) -{ - - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then checks for a good spot in the territory. If none, and town/city phase, checks outside - // The AI will currently not build a CC if it wouldn't connect with an existing CC. - - let obstructions = m.createObstructionMap(gameState, this.accessIndex, template); - - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray(); - - let bestIdx; - let bestVal = 0; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let cellSize = territoryMap.cellSize; - - for (let j of this.territoryIndices) - { - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) // no room around - continue; - - // we add 3 times the needed resource and once the others (except food) - let total = 2*gameState.sharedScript.resourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - total += gameState.sharedScript.resourceMaps[res].map[j]; - - total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 - if (total <= bestVal) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - for (let dp of dpEnts) - { - let dpPos = dp.position(); - if (!dpPos) - continue; - let dist = API3.SquareVectorDistance(dpPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - - for (let cc of ccEnts) - { - let ccPos = cc.position(); - if (!ccPos) - continue; - let dist = API3.SquareVectorDistance(ccPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = total; - bestIdx = i; - } - - if (this.Config.debug > 2) - warn(" for dropsite best is " + bestVal); - - if (bestVal <= 0) - return { "quality": bestVal, "pos": [0, 0] }; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return { "quality": bestVal, "pos": [x, z] }; -}; - -m.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) -{ - let count = 0; - let check = {}; - for (let supply of this.dropsiteSupplies[type].nearby) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - count += supply.ent.resourceSupplyAmount(); - } - if (nearbyOnly) - return count; - - for (let supply of this.dropsiteSupplies[type].medium) - { - if (check[supply.id]) - continue; - check[supply.id] = true; - count += 0.6*supply.ent.resourceSupplyAmount(); - } - return count; -}; - -/** check our resource levels and react accordingly */ -m.BaseManager.prototype.checkResourceLevels = function(gameState, queues) -{ - for (let type of Resources.GetCodes()) - { - if (type == "food") - { - if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations - let numQueue = queues.field.countQueuedUnits(); - - // TODO if not yet farms, add a check on time used/lost and build farmstead if needed - if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them - { - if (count < 600) - { - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID })); - gameState.ai.HQ.needFarm = true; - } - } - else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) - { - let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; - let goal = this.Config.Economy.provisionFields; - if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) - goal = Math.max(goal-1, 1); - if (numFound + numQueue < goal) - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "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 m.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")) - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - if (count < 900) - { - queues.corral.addPlan(new m.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(API3.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 (let ent of this.gatherersByType(gameState, type).values()) - { - if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") - ++this.gatherers[type].used; - else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - ++this.gatherers[type].lost; - } - // TODO add also a test on remaining resources. - let total = this.gatherers[type].used + this.gatherers[type].lost; - if (total > 150 || total > 60 && type != "wood") - { - let ratio = this.gatherers[type].lost / total; - if (ratio > 0.15) - { - let newDP = this.findBestDropsiteLocation(gameState, type); - if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) - { - // No good dropsite, try to build a new base if no base already planned, - // and if not possible, be less strict on dropsite quality. - if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && - newDP.quality > Math.min(25, 50*0.15/ratio) && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - } - } - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; - this.gatherers[type].used = 0; - this.gatherers[type].lost = 0; - } - else if (total == 0) - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; - } - -}; - -/** Adds the estimated gather rates from this base to the currentRates */ -m.BaseManager.prototype.addGatherRates = function(gameState, currentRates) -{ - for (let res in currentRates) - { - // I calculate the exact gathering rate for each unit. - // I must then lower that to account for travel time. - // Given that the faster you gather, the more travel time matters, - // I use some logarithms. - // TODO: this should take into account for unit speed and/or distance to target - - this.gatherersByType(gameState, res).forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - if (res == "food") - { - this.workersBySubrole(gameState, "hunter").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - this.workersBySubrole(gameState, "fisher").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - } - } -}; - -m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) -{ - if (!roleless) - roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); - - for (let ent of roleless) - { - if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "role", "worker"); - else if (ent.hasClass("Support") && ent.hasClass("Elephant")) - ent.setMetadata(PlayerID, "role", "worker"); - } -}; - -/** - * If the numbers of workers on the resources is unbalanced then set some of workers to idle so - * they can be reassigned by reassignIdleWorkers. - * TODO: actually this probably should be in the HQ. - */ -m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) -{ - this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; - // change resource only towards one which is more needed, and if changing will not change this order - let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let sumWanted = 0; - let sumCurrent = 0; - for (let need of mostNeeded) - { - sumWanted += need.wanted; - sumCurrent += need.current; - } - let scale = 1; - if (sumWanted > 0) - scale = sumCurrent / sumWanted; - - for (let i = mostNeeded.length-1; i > 0; --i) - { - let lessNeed = mostNeeded[i]; - for (let j = 0; j < i; ++j) - { - let moreNeed = mostNeeded[j]; - let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) - { - if (lessNeed.type != "food" && gameState.ai.HQ.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" && gameState.ai.HQ.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 FemaleCitizen for food and CitizenSoldier for other resources. - */ -m.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) -{ - let num = number; - let only; - let gatherers = this.gatherersByType(gameState, from); - if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) - only = "CitizenSoldier"; - else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) - only = "FemaleCitizen"; - - for (let 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); - gameState.ai.HQ.AddTCResGatherer(to); - } - return num; -}; - -m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) -{ - // Search for idle workers, and tell them to gather resources based on demand - if (!idleWorkers) - { - let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); - idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); - } - - for (let ent of idleWorkers) - { - // Check that the worker isn't garrisoned - if (!ent.position()) - continue; - // Support elephant can only be builders - if (ent.hasClass("Support") && ent.hasClass("Elephant")) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - continue; - } - - if (ent.hasClass("Worker")) - { - // Just emergency repairing here. It is better managed in assignToFoundations - if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && - gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) - ent.repair(this.anchor); - else if (ent.isGatherer()) - { - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - for (let needed of mostNeeded) - { - if (!ent.canGather(needed.type)) - continue; - let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) - continue; - ent.setMetadata(PlayerID, "subrole", "gatherer"); - ent.setMetadata(PlayerID, "gather-type", needed.type); - gameState.ai.HQ.AddTCResGatherer(needed.type); - break; - } - } - } - else if (ent.hasClass("Cavalry")) - ent.setMetadata(PlayerID, "subrole", "hunter"); - else if (ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "subrole", "fisher"); - } -}; - -m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) -{ - return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); -}; - -m.BaseManager.prototype.gatherersByType = function(gameState, type) -{ - return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); -}; - -/** - * returns an entity collection of workers. - * They are idled immediatly and their subrole set to idle. - */ -m.BaseManager.prototype.pickBuilders = function(gameState, workers, number) -{ - let availableWorkers = this.workers.filter(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") == "builder") - vala = 100; - if (b.getMetadata(PlayerID, "subrole") == "builder") - valb = 100; - if (a.getMetadata(PlayerID, "subrole") == "idle") - vala = -50; - if (b.getMetadata(PlayerID, "subrole") == "idle") - valb = -50; - if (a.getMetadata(PlayerID, "plan") === undefined) - vala = -20; - if (b.getMetadata(PlayerID, "plan") === undefined) - valb = -20; - return vala - valb; - }); - let needed = Math.min(number, availableWorkers.length - 3); - for (let i = 0; i < needed; ++i) - { - availableWorkers[i].stopMoving(); - availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); - workers.addEnt(availableWorkers[i]); - } - return; -}; - -/** - * If we have some foundations, and we don't have enough builder-workers, - * try reassigning some other workers who are nearby - * AI tries to use builders sensibly, not completely stopping its econ. - */ -m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) -{ - let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); - - let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); - - // Check if nothing to build - if (!foundations.length && !damagedBuildings.length) - return; - - let workers = this.workers.filter(ent => ent.isBuilder()); - let builderWorkers = this.workersBySubrole(gameState, "builder"); - let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); - - // if we're constructing and we have the foundations to our base anchor, only try building that. - if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) - { - foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); - let tID = foundations.toEntityArray()[0].id(); - workers.forEach(ent => { - let target = ent.getMetadata(PlayerID, "target-foundation"); - if (target && target != tID) - { - ent.stopMoving(); - ent.setMetadata(PlayerID, "target-foundation", tID); - } - }); - } - - if (workers.length < 3) - { - let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); - if (fromOtherBase) - { - let baseID = this.ID; - fromOtherBase.forEach(worker => { - worker.setMetadata(PlayerID, "base", baseID); - worker.setMetadata(PlayerID, "subrole", "builder"); - workers.updateEnt(worker); - builderWorkers.updateEnt(worker); - idleBuilderWorkers.updateEnt(worker); - }); - } - } - - let builderTot = builderWorkers.length - idleBuilderWorkers.length; - - // Make the limit on number of builders depends on the available resources - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - let builderRatio = 1; - for (let 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 (let target of foundations.values()) - { - if (target.hasClass("Field")) - continue; // we do not build fields - - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) - continue; - - // if our territory has shrinked since this foundation was positioned, do not build it - if (m.isNotWorthBuilding(gameState, target)) - continue; - - let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - let maxTotalBuilders = Math.ceil(workers.length * 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.hasClass("Fortress") || target.hasClass("Wonder") || - target.getMetadata(PlayerID, "phaseUp") == true) - targetNB = 7; - else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || - target.hasClass("Market")) - targetNB = 4; - else if (target.hasClass("House") || target.hasClass("DropsiteWood")) - targetNB = 3; - - if (target.getMetadata(PlayerID, "baseAnchor") == true || - target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) - { - targetNB = 15; - maxTotalBuilders = Math.max(maxTotalBuilders, 15); - } - - // if no base yet, everybody should build - if (gameState.ai.HQ.numActiveBases() == 0) - { - targetNB = workers.length; - maxTotalBuilders = targetNB; - } - - if (assigned >= targetNB) - continue; - idleBuilderWorkers.forEach(function(ent) { - if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (assigned >= targetNB || !ent.position() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }).toEntityArray(); - let time = target.buildTime(); - nonBuilderWorkers.sort((workerA, workerB) => { - let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); - // elephant moves slowly, so when far away they are only useful if build time is long - if (workerA.hasClass("Elephant")) - coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); - else if (workerA.getMetadata(PlayerID, "gather-type") == "food") - coeffA *= 3; - let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); - if (workerB.hasClass("Elephant")) - coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); - else if (workerB.getMetadata(PlayerID, "gather-type") == "food") - coeffB *= 3; - return coeffA - coeffB; - }); - let current = 0; - let nonBuilderTot = nonBuilderWorkers.length; - while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) - { - ++assigned; - ++builderTot; - let ent = nonBuilderWorkers[current++]; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - } - } - - for (let target of damagedBuildings.values()) - { - // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building - // that's being destroyed. - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - { - if (target.healthLevel() > 0.5 || - !target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!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.hasClass("Fortress") || target.hasClass("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() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }); - let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); - let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); - - nearestNonBuilders.forEach(function(ent) { - ++assigned; - ++builderTot; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - } -}; - -/** Return false when the base is not active (no workers on it) */ -m.BaseManager.prototype.update = function(gameState, queues, events) -{ - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units - { - // if some active base, reassigns the workers/buildings - // otherwise look for anything useful to do, i.e. treasures to gather - if (gameState.ai.HQ.numActiveBases() > 0) - { - for (let ent of this.units.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (bestBase.ID != this.ID) - bestBase.assignEntity(gameState, ent); - } - for (let ent of this.buildings.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (!bestBase) - { - if (ent.hasClass("Dock")) - API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); - continue; - } - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - 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 (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let 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 (let ent of this.units.values()) - { - let base = m.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 (let ent of this.buildings.values()) - { - if (!ent.position()) - continue; - let base = m.getBestBase(gameState, ent); - if (base.anchor) - reassignedBase = base; - break; - } - - if (reassignedBase) - { - for (let ent of this.units.values()) - reassignedBase.assignEntity(gameState, ent); - for (let ent of this.buildings.values()) - { - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - reassignedBase.assignEntity(gameState, ent); - } - return false; - } - - this.assignToFoundations(gameState); - if (gameState.ai.elapsedTime > this.timeNextIdleCheck) - this.setWorkersIdleByPriority(gameState); - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - return true; - } - - Engine.ProfileStart("Base update - base " + this.ID); - - this.checkResourceLevels(gameState, queues); - this.assignToFoundations(gameState); - - if (this.constructing) - { - let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); - if(owner != 0 && !gameState.isPlayerAlly(owner)) - { - // we're in enemy territory. If we're too close from the enemy, destroy us. - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (cc.owner() != owner) - continue; - if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) - continue; - this.anchor.destroy(); - gameState.ai.HQ.resetBaseCache(); - break; - } - } - } - else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) - --this.neededDefenders; - - if (gameState.ai.elapsedTime > this.timeNextIdleCheck && - (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) - this.setWorkersIdleByPriority(gameState); - - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - // check if workers can find something useful to do - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - - Engine.ProfileStop(); - return true; -}; - -m.BaseManager.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "anchorId": this.anchorId, - "accessIndex": this.accessIndex, - "maxDistResourceSquare": this.maxDistResourceSquare, - "constructing": this.constructing, - "gatherers": this.gatherers, - "neededDefenders": this.neededDefenders, - "territoryIndices": this.territoryIndices, - "timeNextIdleCheck": this.timeNextIdleCheck - }; -}; - -m.BaseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-single-base/buildManager.js b/install/petraBased/petra-single-base/buildManager.js deleted file mode 100644 index fa78ca2..0000000 --- a/install/petraBased/petra-single-base/buildManager.js +++ /dev/null @@ -1,173 +0,0 @@ -var PETRA = function(m) -{ - -/** - * 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. - */ - -m.BuildManager = function() -{ - // List of buildings we have builders for, with number of possible builders. - this.builderCounters = 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(); -}; - -/** Initialization at start of game */ -m.BuildManager.prototype.init = function(gameState) -{ - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnUnits().values()) - this.incrementBuilderCounters(civ, ent, 1); -}; - -m.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) -{ - for (let buildable of ent.buildableEntities(civ)) - { - if (this.builderCounters.has(buildable)) - { - let count = this.builderCounters.get(buildable) + increment; - if (count < 0) - { - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); - continue; - } - this.builderCounters.set(buildable, count); - } - else if (increment > 0) - this.builderCounters.set(buildable, increment); - else - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); - } -}; - -/** Update the builders counters */ -m.BuildManager.prototype.checkEvents = function(gameState, events) -{ - this.elapsedTime = gameState.ai.elapsedTime; - let civ = gameState.getPlayerCiv(); - - for (let evt of events.Create) - { - if (events.Destroy.some(e => e.entity == evt.entity)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, 1); - } - - for (let evt of events.Destroy) - { - if (events.Create.some(e => e.entity == evt.entity) || !evt.entityObj) - continue; - let ent = evt.entityObj; - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, -1); - } - - for (let evt of events.OwnershipChanged) // capture events - { - let increment; - if (evt.from == PlayerID) - increment = -1; - else if (evt.to == PlayerID) - increment = 1; - else - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, increment); - } -}; - - -/** - * Get the first buildable structure with a given class - * TODO when several available, choose the best one - */ -m.BuildManager.prototype.findStructureWithClass = function(gameState, classes) -{ - for (let [templateName, count] of this.builderCounters) - { - if (count == 0 || gameState.isTemplateDisabled(templateName)) - continue; - let template = gameState.getTemplate(templateName); - if (!template || !template.available(gameState)) - continue; - if (MatchesClassList(template.classes(), classes)) - return templateName; - } - return undefined; -}; - -m.BuildManager.prototype.hasBuilder = function(template) -{ - let numBuilders = this.builderCounters.get(template); - return numBuilders && numBuilders > 0; -}; - -m.BuildManager.prototype.isUnbuildable = function(gameState, template) -{ - return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; -}; - -m.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 */ -m.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 - { - let 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 */ -m.BuildManager.prototype.numberMissingRoom = function(gameState) -{ - let num = 0; - for (let unbuildable of this.unbuildables.values()) - if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) - ++num; - return num; -}; - -/** Reset the unbuildables due to missing room */ -m.BuildManager.prototype.resetMissingRoom = function(gameState) -{ - for (let [key, unbuildable] of this.unbuildables) - if (unbuildable.reason == "room") - this.unbuildables.delete(key); -}; - -m.BuildManager.prototype.Serialize = function() -{ - return { - "builderCounters": this.builderCounters, - "unbuildables": this.unbuildables - }; -}; - -m.BuildManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/chatHelper.js b/install/petraBased/petra-single-base/chatHelper.js deleted file mode 100644 index 8afbcfc..0000000 --- a/install/petraBased/petra-single-base/chatHelper.js +++ /dev/null @@ -1,242 +0,0 @@ -var PETRA = function(m) -{ - -m.launchAttackMessages = { - "hugeAttack": [ - markForTranslation("I am starting a massive military campaign against %(_player_)s, come and join me."), - markForTranslation("I have set up a huge army to crush %(_player_)s. Join me and you will have your share of the loot.") - ], - "other": [ - markForTranslation("I am launching an attack against %(_player_)s."), - markForTranslation("I have just sent an army against %(_player_)s.") - ] -}; - -m.answerRequestAttackMessages = { - "join": [ - markForTranslation("Let me regroup my army and I will then join you against %(_player_)s."), - markForTranslation("I am finishing preparations to attack %(_player_)s.") - ], - "decline": [ - markForTranslation("Sorry, I do not have enough soldiers currently; but my next attack will target %(_player_)s."), - markForTranslation("Sorry, I still need to strengthen my army. However, I will attack %(_player_)s next.") - ], - "other": [ - markForTranslation("I cannot help you against %(_player_)s for the time being, I am planning to attack %(_player_2)s first.") - ] -}; - -m.sentTributeMessages = [ - markForTranslation("Here is a gift for you, %(_player_)s. Make good use of it."), - markForTranslation("I see you are in a bad situation, %(_player_)s. I hope this helps."), - markForTranslation("I can help you this time, %(_player_)s, but you should manage your resources more carefully in the future.") -]; - -m.requestTributeMessages = [ - markForTranslation("I am in need of %(resource)s, can you help? I will make it up to you."), - markForTranslation("I would participate more efficiently in our common war effort if you could provide me some %(resource)s."), - markForTranslation("If you can spare me some %(resource)s, I will be able to strengthen my army.") -]; - -m.newTradeRouteMessages = [ - markForTranslation("I have set up a new route with %(_player_)s. Trading will be profitable for all of us."), - markForTranslation("A new trade route is set up with %(_player_)s. Take your share of the profits.") -]; - -m.newDiplomacyMessages = { - "ally": [ - markForTranslation("%(_player_)s and I are now allies.") - ], - "neutral": [ - markForTranslation("%(_player_)s and I are now neutral.") - ], - "enemy": [ - markForTranslation("%(_player_)s and I are now enemies.") - ] -}; - -m.answerDiplomacyRequestMessages = { - "ally": { - "decline": [ - markForTranslation("I cannot accept your offer to become allies, %(_player_)s.") - ], - "declineSuggestNeutral": [ - markForTranslation("I will not be your ally, %(_player_)s. However, I will consider a neutrality pact."), - markForTranslation("I reject your request for alliance, %(_player_)s, but we could become neutral."), - markForTranslation("%(_player_)s, only a neutrality agreement is conceivable to me.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous alliance did not work out, %(_player_)s. I must decline your offer."), - markForTranslation("I won’t ally you again, %(_player_)s!"), - markForTranslation("No more alliances between us, %(_player_)s!"), - markForTranslation("Your request for peace means nothing to me anymore, %(_player_)s!"), - markForTranslation("My answer to your repeated peace proposal will remain war, %(_player_)s!") - ], - "accept": [ - markForTranslation("I will accept your offer to become allies, %(_player_)s. We will both benefit from this partnership."), - markForTranslation("An alliance between us is a good idea, %(_player_)s."), - markForTranslation("Let both of our people prosper from a peaceful association, %(_player_)s."), - markForTranslation("We have found common ground, %(_player_)s. I accept the alliance."), - markForTranslation("%(_player_)s, consider us allies from now on.") - ], - "acceptWithTribute": [ - markForTranslation("I will ally with you, %(_player_)s, but only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("%(_player_)s, you must send me a tribute of %(_amount_)s %(_resource_)s before I accept an alliance with you."), - markForTranslation("Unless you send me %(_amount_)s %(_resource_)s, an alliance won’t be formed, %(_player_)s,") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, my offer still stands. I will ally with you only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("I’m still waiting for %(_amount_)s %(_resource_)s before accepting your alliance, %(_player_)s."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - }, - "neutral": { - "decline": [ - markForTranslation("I will not become neutral with you, %(_player_)s."), - markForTranslation("%(_player_)s, I must decline your request for a neutrality pact.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous neutrality agreement ended in failure, %(_player_)s; I will not consider another one.") - ], - "accept": [ - markForTranslation("I welcome your request for peace between our civilizations, %(_player_)s. I will accept."), - markForTranslation("%(_player_)s, I will accept your neutrality request. May both our civilizations benefit.") - ], - "acceptWithTribute": [ - markForTranslation("If you send me a tribute of %(_amount_)s %(_resource_)s, I will accept your neutrality request, %(_player_)s."), - markForTranslation("%(_player_)s, if you send me %(_amount_)s %(_resource_)s, I will accept a neutrality pact.") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, I will not accept your neutrality request unless you tribute me %(_amount_)s %(_resource_)s soon."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - } -}; - -m.sendDiplomacyRequestMessages = { - "ally": { - "sendRequest": [ - markForTranslation("%(_player_)s, it would help both of our civilizations if we formed an alliance. If you become allies with me, I will respond in kind.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, my offer for an alliance has expired."), - markForTranslation("%(_player_)s, I have rescinded my previous offer for an alliance between us."), - ] - }, - "neutral": { - "sendRequest": [ - markForTranslation("%(_player_)s, I would like to request a neutrality pact between our civilizations. If you become neutral with me, I will respond in kind."), - markForTranslation("%(_player_)s, it would be both to our benefit if we negotiated a neutrality pact. I will become neutral with you if you do the same.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, I have decided to revoke my offer for a neutrality pact."), - markForTranslation("%(_player_)s, as you have failed to respond to my request for peace between us, I have abrogated my offer."), - ] - } -}; - -m.chatLaunchAttack = function(gameState, player, type) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.launchAttackMessages[type === "HugeAttack" ? "hugeAttack" : "other"]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestAttack = function(gameState, player, answer, other) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.answerRequestAttackMessages[answer]), - "translateMessage": true, - "translateParameters": answer != "other" ? ["_player_"] : ["_player_", "_player_2"], - "parameters": answer != "other" ? { "_player_": player } : { "_player_": player, "_player_2": other } - }); -}; - -m.chatSentTribute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.sentTributeMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatRequestTribute = function(gameState, resource) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.requestTributeMessages), - "translateMessage": true, - "translateParameters": { "resource": "withinSentence" }, - "parameters": { "resource": Resources.GetNames()[resource] } - }); -}; - -m.chatNewTradeRoute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newTradeRouteMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatNewPhase = function(gameState, phase, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newPhaseMessages[status]), - "translateMessage": true, - "translateParameters": ["phase"], - "parameters": { "phase": phase } - }); -}; - -m.chatNewDiplomacy = function(gameState, player, newDiplomaticStance) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": pickRandom(this.newDiplomacyMessages[newDiplomaticStance]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestDiplomacy = function(gameState, player, requestType, response, requiredTribute) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.answerDiplomacyRequestMessages[requestType][response]), - "translateMessage": true, - "translateParameters": requiredTribute ? ["_amount_", "_resource_", "_player_"] : ["_player_"], - "parameters": requiredTribute ? - { "_amount_": requiredTribute.wanted, "_resource_": requiredTribute.type, "_player_": player } : - { "_player_": player } - }); -}; - -m.chatNewRequestDiplomacy = function(gameState, player, requestType, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.sendDiplomacyRequestMessages[requestType][status]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/config.js b/install/petraBased/petra-single-base/config.js deleted file mode 100644 index 77e4b56..0000000 --- a/install/petraBased/petra-single-base/config.js +++ /dev/null @@ -1,257 +0,0 @@ -var PETRA = function(m) -{ - -m.Config = function(difficulty, behavior) -{ - // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. - this.difficulty = difficulty !== undefined ? difficulty : 3; - - // 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; - - this.chat = true; // false to prevent AI's chats - - this.popScaling = 1; // scale factor depending on the max population - - this.Military = { - "towerLapseTime": 90, // Time to wait between building 2 towers - "fortressLapseTime": 390, // Time to wait between building 2 fortresses - "popForBarracks1": 25, - "popForBarracks2": 95, - "popForBlacksmith": 65, - "numSentryTowers": 1 - }; - this.Economy = { - "popPhase2": 38, // How many units we want before aging to phase2. - "workPhase3": 65, // How many workers we want before aging to phase3. - "workPhase4": 80, // How many workers we want before aging to phase4 or higher. - "popForDock": 25, - "targetNumWorkers": 40, // dummy, will be changed later - "targetNumTraders": 5, // Target number of traders - "targetNumFishers": 1, // Target number of fishers per sea - "supportRatio": 0.35, // fraction of support workers among the workforce - "provisionFields": 2 - }; - - // Note: attack settings are set directly in attack_plan.js - // defense - this.Defense = - { - "defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. - "armyCompactSize": 2000, // squared. Half-diameter of an army. - "armyBreakawaySize": 3500, // squared. - "armyMergeSize": 1400 // squared. - }; - - // 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 = - { - "default": [], - "athen": ["structures/{civ}_gymnasion", "structures/{civ}_prytaneion", - "structures/{civ}_theatron", "structures/{civ}_royal_stoa"], - "brit": ["structures/{civ}_rotarymill"], - "cart": ["structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", - "structures/{civ}_embassy_italiote"], - "gaul": ["structures/{civ}_rotarymill", "structures/{civ}_tavern"], - "iber": ["structures/{civ}_monument"], - "kush": ["structures/{civ}_pyramid_large", "structures/{civ}_blemmye_camp", - "structures/{civ}_nuba_village"], - "mace": ["structures/{civ}_library", "structures/{civ}_theatron"], - "maur": ["structures/{civ}_pillar_ashoka"], - "pers": ["structures/{civ}_apadana", "structures/{civ}_hall"], - "ptol": ["structures/{civ}_library"], - "rome": ["structures/{civ}_army_camp"], - "sele": ["structures/{civ}_library"], - "spart": ["structures/{civ}_syssiton", "structures/{civ}_theatron", - "structures/{civ}_royal_stoa"] - }; - - this.priorities = - { - "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food - "citizenSoldier": 60, - "trader": 50, - "healer": 20, - "ships": 70, - "house": 350, - "dropsites": 200, - "field": 400, - "dock": 90, - "corral": 100, - "economicBuilding": 90, - "militaryBuilding": 130, - "defenseBuilding": 70, - "civilCentre": 950, - "majorTech": 700, - "minorTech": 40, - "wonder": 1000, - "emergency": 1000 // used only in emergency situations, should be the highest one - }; - - // Default personality (will be updated in setConfig) - this.personality = - { - "aggressive": 0.5, - "cooperative": 0.5, - "defensive": 0.5 - }; - - // See m.QueueManager.prototype.wantedGatherRates() - this.queues = - { - "firstTurn": { - "food": 10, - "wood": 10, - "default": 0 - }, - "short": { - "food": 200, - "wood": 200, - "default": 100 - }, - "medium": { - "default": 0 - }, - "long": { - "default": 0 - } - }; - - this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; -}; - -m.Config.prototype.setConfig = function(gameState) -{ - if (this.difficulty > 0) - { - // 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 - let 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 } - }; - let behavior = randFloat(-0.5, 0.5); - // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety - let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); - let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; - let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; - let min = personalityList[this.behavior].min; - let 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 < 2) - { - this.Economy.supportRatio = 0.5; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else if (this.difficulty < 3) - { - this.Economy.supportRatio = 0.4; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else - { - if (this.difficulty == 3) - 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; - } - } - - let maxPop = gameState.getPopulationMax(); - if (this.difficulty < 2) - this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); - else if (this.difficulty < 3) - this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); - else - this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); - this.Economy.targetNumTraders = 2 + this.difficulty; - - - if (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.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * 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 < 2) - this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase - - if (this.debug < 2) - return; - API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); -}; - -m.Config.prototype.Serialize = function() -{ - var data = {}; - for (let key in this) - if (this.hasOwnProperty(key) && key != "debug") - data[key] = this[key]; - return data; -}; - -m.Config.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/defenseArmy.js b/install/petraBased/petra-single-base/defenseArmy.js deleted file mode 100644 index a5be2f2..0000000 --- a/install/petraBased/petra-single-base/defenseArmy.js +++ /dev/null @@ -1,651 +0,0 @@ -var PETRA = function(m) -{ - -/** Armies used by the defense manager. - * An army is a collection of own entities and enemy entities. - * - * Types of armies: - * "default": army to counter an invading army - * "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 - */ -m.DefenseArmy = function(gameState, foeEntities, type) -{ - 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 (let 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 - */ -m.DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) -{ - if (this.foeEntities.indexOf(enemyId) !== -1) - return false; - let ent = gameState.getEntityById(enemyId); - if (!ent || !ent.position()) - return false; - - // check distance - if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) - return false; - - this.foeEntities.push(enemyId); - this.assignedAgainst[enemyId] = []; - this.positionLastUpdate = 0; - this.evaluateStrength(ent); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - - return true; -}; - -/** - * returns true if the entity was removed and false otherwise. - * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. - */ -m.DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) -{ - let idx = this.foeEntities.indexOf(enemyId); - if (idx === -1) - return false; - - this.foeEntities.splice(idx, 1); - - this.assignedAgainst[enemyId] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] == enemyId) - this.assignedTo[to] = undefined; - - let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); - if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) - { - this.evaluateStrength(ent, false, true); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - } - - return true; -}; - -/** - * adds a defender but doesn't assign him yet. - * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship - */ -m.DefenseArmy.prototype.addOwn = function(gameState, id, force) -{ - if (this.ownEntities.indexOf(id) !== -1) - return false; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position() && !force) - return false; - - this.ownEntities.push(id); - this.evaluateStrength(ent, true); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - this.assignedTo[id] = 0; - - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole === undefined || subrole !== "defender") - ent.setMetadata(PlayerID, "formerSubrole", subrole); - ent.setMetadata(PlayerID, "subrole", "defender"); - return true; -}; - -m.DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) -{ - let idx = this.ownEntities.indexOf(id); - if (idx === -1) - return false; - - this.ownEntities.splice(idx, 1); - - if (this.assignedTo[id] !== 0) - { - let temp = this.assignedAgainst[this.assignedTo[id]]; - if (temp) - temp.splice(temp.indexOf(id), 1); - } - this.assignedTo[id] = undefined; - - let ent = Entity ? Entity : gameState.getEntityById(id); - if (!ent) - 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); - - let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); - if (formerSubrole !== undefined) - ent.setMetadata(PlayerID, "subrole", formerSubrole); - else - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "formerSubrole", undefined); - - // Remove from tranport plan if not yet on Board - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan && plan.state == "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. - */ -m.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 - let posOwn = [0, 0]; - let nOwn = 0; - let posAlly = [0, 0]; - let nAlly = 0; - let posOther = [0, 0]; - let nOther = 0; - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - let pos = ent.position(); - let 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; - let armyAccess = gameState.ai.accessibility.getAccessValue(posOther); - for (let struct of gameState.getAllyStructures().values()) - { - let pos = struct.position(); - if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) - continue; - if (m.getLandAccess(gameState, struct) !== armyAccess) - continue; - let defensiveStruct = struct.hasDefensiveFire(); - if (defensiveFound && !defensiveStruct) - continue; - let dist = API3.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) - { - let entId = this.ownEntities[0]; - this.removeOwn(gameState, entId); - let 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); -}; - -m.DefenseArmy.prototype.assignUnit = function(gameState, entID) -{ - // we'll assume this defender is ours already. - // we'll also override any previous assignment - - let ent = gameState.getEntityById(entID); - if (!ent || !ent.position()) - return false; - - // try to return its resources, and if any, the attack order will be queued - let queued = m.returnResources(gameState, ent); - - let idMin; - let distMin; - let idMinAll; - let distMinAll; - for (let id of this.foeEntities) - { - let eEnt = gameState.getEntityById(id); - if (!eEnt || !eEnt.position()) // probably can't happen. - 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") && !m.isSiegeUnit(eEnt)) - continue; - - let dist = API3.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; - - let ownIndex = m.getLandAccess(gameState, ent); - let foeEnt = gameState.getEntityById(idFoe); - let foePosition = foeEnt.position(); - let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); - if (ownIndex == foeIndex || ent.hasClass("Ship")) - { - this.assignedTo[entID] = idFoe; - this.assignedAgainst[idFoe].push(entID); - ent.attack(idFoe, m.allowCapture(gameState, ent, foeEnt), queued); - } - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); - return true; -}; - -m.DefenseArmy.prototype.getType = function() -{ - return this.type; -}; - -m.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). - */ -m.DefenseArmy.prototype.merge = function(gameState, otherArmy) -{ - // copy over all parameters. - for (let i in otherArmy.assignedAgainst) - { - if (this.assignedAgainst[i] === undefined) - this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; - else - this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); - } - for (let i in otherArmy.assignedTo) - this.assignedTo[i] = otherArmy.assignedTo[i]; - - for (let id of otherArmy.foeEntities) - this.addFoe(gameState, id, true); - // TODO: reassign those ? - for (let id of otherArmy.ownEntities) - this.addOwn(gameState, id, true); - - this.recalculatePosition(gameState, true); - this.recalculateStrengths(gameState); - - return true; -}; - -m.DefenseArmy.prototype.needsDefenders = function(gameState) -{ - let defenseRatio; - let 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. */ -m.DefenseArmy.prototype.recalculatePosition = function(gameState, force) -{ - if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) - return; - - let npos = 0; - let pos = [0, 0]; - for (let id of this.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - npos++; - let epos = ent.position(); - pos[0] += epos[0]; - pos[1] += epos[1]; - } - // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position - if (npos > 0) - { - this.foePosition[0] = pos[0]/npos; - this.foePosition[1] = pos[1]/npos; - } - - this.positionLastUpdate = gameState.ai.elapsedTime; -}; - -m.DefenseArmy.prototype.recalculateStrengths = function(gameState) -{ - this.ownStrength = 0; - this.foeStrength = 0; - - for (let id of this.foeEntities) - this.evaluateStrength(gameState.getEntityById(id)); - for (let id of this.ownEntities) - this.evaluateStrength(gameState.getEntityById(id), true); -}; - -/** adds or remove the strength of the entity either to the enemy or to our units. */ -m.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 = m.getMaxStrength(ent); - - // TODO adapt the getMaxStrength function for animals. - // For the time being, just increase it for elephants as the returned value is too small. - if (ent.hasClass("Animal") && ent.hasClass("Elephant")) - entStrength *= 3; - - if (remove) - entStrength *= -1; - - if (isOwn) - this.ownStrength += entStrength; - else - this.foeStrength += entStrength; -}; - -m.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 (let evt of events.EntityRenamed) // take care of promoted and packed units - { - if (this.foeEntities.indexOf(evt.entity) !== -1) - { - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed - continue; - let idx = this.foeEntities.indexOf(evt.entity); - this.foeEntities[idx] = evt.newentity; - this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; - this.assignedAgainst[evt.entity] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] === evt.entity) - this.assignedTo[to] = evt.newentity; - } - else if (this.ownEntities.indexOf(evt.entity) !== -1) - { - let idx = this.ownEntities.indexOf(evt.entity); - this.ownEntities[idx] = evt.newentity; - this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; - this.assignedTo[evt.entity] = undefined; - for (let against in this.assignedAgainst) - { - if (!this.assignedAgainst[against]) - continue; - if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) - this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; - } - } - } - - for (let evt of events.Garrison) - this.removeFoe(gameState, evt.entity); - - for (let evt of events.OwnershipChanged) // captured - { - if (!gameState.isPlayerEnemy(evt.to)) - this.removeFoe(gameState, evt.entity); - else if (evt.from === PlayerID) - this.removeOwn(gameState, evt.entity); - } - - for (let evt of events.Destroy) - { - let entityObj = evt.entityObj || undefined; - // we may have capture+destroy, so do not trust owner and check all possibilities - this.removeOwn(gameState, evt.entity, entityObj); - this.removeFoe(gameState, evt.entity, entityObj); - } -}; - -m.DefenseArmy.prototype.update = function(gameState) -{ - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent) - continue; - let 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") - { - let target = gameState.getEntityById(orderData[0].target); - if (target && !m.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) - let 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 []; - } - - let breakaways = []; - // TODO: assign unassigned defenders, cleanup of a few things. - // perhaps occasional strength recomputation - - // occasional update or breakaways, positions… - if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) - { - this.recalculatePosition(gameState); - this.positionLastUpdate = gameState.ai.elapsedTime; - - // Check for breakaways. - for (let i = 0; i < this.foeEntities.length; ++i) - { - let id = this.foeEntities[i]; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) - { - breakaways.push(id); - if (this.removeFoe(gameState, id)) - i--; - } - } - - this.recalculatePosition(gameState); - } - - return breakaways; -}; - -m.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 - }; -}; - -m.DefenseArmy.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/defenseManager.js b/install/petraBased/petra-single-base/defenseManager.js deleted file mode 100644 index 0d18897..0000000 --- a/install/petraBased/petra-single-base/defenseManager.js +++ /dev/null @@ -1,957 +0,0 @@ -var PETRA = function(m) -{ - -m.DefenseManager = function(Config) -{ - this.armies = []; // array of "army" Objects - 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 = {}; -}; - -m.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) - { - let 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 = {}; - let attackingArmies = clone(this.attackingArmies); - for (let enemy in this.attackingUnits) - { - if (!this.attackingUnits[enemy]) - continue; - for (let ally in this.attackingUnits[enemy]) - { - 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 (let enemy in attackingArmies) - { - for (let 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(); -}; - -m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") -{ - if (type == "default") - { - // Try to add it to an existing army. - for (let army of this.armies) - if (army.getType() == type && army.addFoe(gameState, entityID)) - return; // over - } - - // Create a new army for it. - let army = new m.DefenseArmy(gameState, [entityID], type); - - this.armies.push(army); -}; - -m.DefenseManager.prototype.getArmy = function(partOfArmy) -{ - // Find the army corresponding to this ID partOfArmy - for (let army of this.armies) - if (army.ID == partOfArmy) - return army; - - return undefined; -}; - -m.DefenseManager.prototype.isDangerous = function(gameState, entity) -{ - if (!entity.position()) - return false; - - let 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") - { - let targetId = entity.unitAIOrderData()[0].target; - if (this.targetList.indexOf(targetId) != -1) - return true; - let target = gameState.getEntityById(targetId); - if (target) - { - let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); - if (isTargetEnemy && territoryOwner == PlayerID) - { - if (target.hasClass("Structure")) - this.targetList.push(targetId); - return true; - } - else if (isTargetEnemy && target.hasClass("CivCentre")) - { - let myBuildings = gameState.getOwnStructures(); - for (let building of myBuildings.values()) - { - if (building.foundationProgress() == 0) - continue; - if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) - continue; - this.targetList.push(targetId); - return true; - } - } - } - } - - 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 (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position()) // the enemy base is either destroyed or built - continue; - if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) - return true; - } - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) - continue; - let cooperation = this.GetCooperationLevel(cc.owner()); - if (cooperation < 0.6 && cc.foundationProgress() !== undefined) - continue; - if (cooperation < 0.3) - continue; - if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) - return true; - } - - for (let building of gameState.getOwnStructures().values()) - { - if (building.foundationProgress() == 0 || - API3.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 (let building of gameState.getAllyStructures(territoryOwner).values()) - { - if (building.foundationProgress() == 0 || - API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) - continue; - if (!this.territoryMap.isBlinking(building.position())) - return true; - } - } - - // Update the number of enemies attacking this ally - let 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; -}; - -m.DefenseManager.prototype.checkEnemyUnits = function(gameState) -{ - const nbPlayers = gameState.sharedScript.playersData.length; - let i = gameState.ai.playedTurn % nbPlayers; - this.attackingUnits[i] = undefined; - - if (i == PlayerID) - { - if (!this.armies.length) - { - // check if we can recover capture points from any of our notdecaying structures - for (let ent of gameState.getOwnStructures().values()) - { - if (ent.decaying()) - continue; - let 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; - - // loop through enemy units - for (let 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; - let orders = ent.unitAIOrderData(); - if (!orders || !orders.length || !orders[0].target) - continue; - let target = gameState.getEntityById(orders[0].target); - if (!target || !gameState.isPlayerAlly(target.owner())) - continue; - } - - // TODO what to do for ships ? - if (ent.hasClass("Ship") || ent.hasClass("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.numActiveBases() == 0) - 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 (let ent of gameState.getEnemyStructures(i).values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - continue; - if (!ent.capturePoints() && !ent.hasDefensiveFire()) - continue; - let owner = this.territoryMap.getOwner(ent.position()); - if (owner == PlayerID) - this.makeIntoArmy(gameState, ent.id(), "capturing"); - } -}; - -m.DefenseManager.prototype.checkEnemyArmies = function(gameState) -{ - for (let i = 0; i < this.armies.length; ++i) - { - let army = this.armies[i]; - // this returns a list of IDs: the units that broke away from the army for being too far. - let breakaways = army.update(gameState); - for (let breaker of breakaways) - this.makeIntoArmy(gameState, breaker); // assume dangerosity - - if (army.getState() == 0) - { - if (army.getType() == "default") - this.switchToAttack(gameState, army); - army.clear(gameState); - this.armies.splice(i--, 1); - continue; - } - } - // Check if we can't merge it with another - for (let i = 0; i < this.armies.length - 1; ++i) - { - let army = this.armies[i]; - if (army.getType() != "default") - continue; - for (let j = i+1; j < this.armies.length; ++j) - { - let otherArmy = this.armies[j]; - if (otherArmy.getType() != "default" || - API3.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) - { - let army = this.armies[i]; - army.recalculatePosition(gameState); - let owner = this.territoryMap.getOwner(army.foePosition); - if (!gameState.isPlayerEnemy(owner)) - { - if (gameState.isPlayerMutualAlly(owner)) - { - // update the number of enemies attacking this ally - for (let id of army.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent) - continue; - let 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; - } - else if (owner != 0) // enemy army back in its territory - { - 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; - let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let base of bases.values()) - { - if (!gameState.isEntityAlly(base)) - continue; - let cooperation = this.GetCooperationLevel(base.owner()); - if (cooperation < 0.3 && !gameState.isEntityOwn(base)) - continue; - if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) - continue; - if(this.Config.debug > 1) - API3.warn("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 (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (API3.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); - } -}; - -m.DefenseManager.prototype.assignDefenders = function(gameState) -{ - if (!this.armies.length) - return; - - let armiesNeeding = []; - // let's add defenders - for (let army of this.armies) - { - let needsDef = army.needsDefenders(gameState); - if (needsDef === false) - continue; - - let armyAccess; - for (let entId of army.foeEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - armyAccess = m.getLandAccess(gameState, ent); - break; - } - if (!armyAccess) - API3.warn(" 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 - let 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.hasClass("Catapult")) - return; - if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) - 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || 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) - { - let ent = gameState.getEntityById(potentialDefenders[i]); - if (!ent || !ent.position()) - continue; - let aMin; - let distMin; - let access = ipass == 0 ? m.getLandAccess(gameState, ent) : undefined; - for (let a = 0; a < armiesNeeding.length; ++a) - { - if (access && armiesNeeding[a].access != access) - continue; - let dist = API3.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 -= m.getMaxStrength(ent); - 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 centre - let armiesPos = []; - for (let a = 0; a < armiesNeeding.length; ++a) - armiesPos.push(armiesNeeding[a].army.foePosition); - gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); -}; - -m.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 - */ -m.DefenseManager.prototype.checkEvents = function(gameState, events) -{ - // must be called every turn for all armies - for (let army of this.armies) - army.checkEvents(gameState, events); - - for (let evt of events.OwnershipChanged) // capture events - { - if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) - { - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("CivCentre")) // one of our cc has been captured - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); - } - } - - let allAttacked = {}; - for (let evt of events.Attacked) - allAttacked[evt.target] = evt.attacker; - - for (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position()) - continue; - - let 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")) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (target.isIdle() || unitAIStateOrder == "GATHER") - { - let pos = attacker.position(); - let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; - if (range * range > API3.SquareVectorDistance(pos, target.position())) - target.moveToRange(pos[0], pos[1], range, range); - } - continue; - } - - if (target.hasClass("Ship")) // TODO integrate other ships later, need to be sure it is accessible - 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")) - { - let 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)) - { - let capture = target.capturePoints(); - if (!capture) - continue; - let 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 - let plan = target.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && attack.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.hasClass("Structure") && !attacker.hasClass("Ship")) - { - let territoryOwner = this.territoryMap.getOwner(attacker.position()); - if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) - this.makeIntoArmy(gameState, attacker.id()); - } - - if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) - { - let 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; - let 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 healthlevel - 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 catapult - if (target.hasClass("Catapult") && - !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 if we should retaliate or continue our task - if (target.hasClass("Support") || target.attackTypes() === undefined) - continue; - let orderData = target.unitAIOrderData(); - let currentTarget = orderData && orderData.length && orderData[0].target ? - gameState.getEntityById(orderData[0].target) : undefined; - if (currentTarget) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || - !currentTarget.hasClass("Structure") && !currentTarget.hasClass("Support"))) - continue; - if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) - continue; - if (unitAIStateOrder == "COMBAT" && !m.isSiegeUnit(currentTarget) && - gameState.ai.HQ.capturableTargets.has(orderData[0].target)) - { - // take the nearest unit also attacking this structure to help us - let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); - let minDist; - let minEnt; - let pos = attacker.position(); - capturableTarget.ents.delete(target.id()); - for (let entId of capturableTarget.ents) - { - if (allAttacked[entId]) - continue; - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - // Check that the unit is still attacking the structure (since the last played turn) - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let entOrderData = ent.unitAIOrderData(); - if (!entOrderData || !entOrderData.length || !entOrderData[0].target || - entOrderData[0].target != orderData[0].target) - continue; - let dist = API3.SquareVectorDistance(pos, ent.position()); - if (minEnt && dist > minDist) - continue; - minDist = dist; - minEnt = ent; - } - if (minEnt) - { - capturableTarget.ents.delete(minEnt.id()); - minEnt.attack(attacker.id(), m.allowCapture(gameState, minEnt, attacker)); - } - } - } - target.attack(attacker.id(), m.allowCapture(gameState, target, attacker)); - } - } -}; - -m.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) -{ - if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) - return false; - let minGarrison = data.min || target.garrisonMax(); - if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - return false; - if (data.attacker) - { - let attackTypes = target.attackTypes(); - if (!attackTypes || attackTypes.indexOf("Ranged") == -1) - return false; - let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); - let range = target.attackRange("Ranged").max; - if (dist >= range*range) - return false; - } - let access = m.getLandAccess(gameState, target); - let garrisonManager = gameState.ai.HQ.garrisonManager; - let garrisonArrowClasses = target.getGarrisonArrowClasses(); - let typeGarrison = data.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") : !m.isSiegeUnit(data.attacker); - else - allowMelee = true; - } - let units = gameState.getOwnUnits().filter(ent => { - if (!ent.position()) - return false; - if (!MatchesClassList(ent.classes(), garrisonArrowClasses)) - return false; - if (typeGarrison != "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) - return false; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return false; - let 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) - { - let 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 != "decay" && subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(target.position()); - - let ret = false; - for (let ent of units.values()) - { - if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - break; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - let 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; -}; - -/** garrison a attacked siege ranged unit inside the nearest fortress */ -m.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (nearest) - garrisonManager.garrison(gameState, unit, nearest, "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 - */ -m.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!emergency && !ent.buffHeal()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && - (!emergency || !ent.garrisoned().length)) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (!nearest) - return false; - - if (!emergency) - { - garrisonManager.garrison(gameState, unit, nearest, "protection"); - return true; - } - if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent - nearest.unload(nearest.garrisoned()[0]); - - garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); - return true; -}; - -/** - * Be more inclined to help an ally attacked by several enemies - */ -m.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 - */ -m.DefenseManager.prototype.switchToAttack = function(gameState, army) -{ - if (!army) - return; - for (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) - continue; - let targetAccess = m.getLandAccess(gameState, target); - let targetPos = target.position(); - for (let entId of army.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position() || m.getLandAccess(gameState, ent) != targetAccess) - continue; - if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) - continue; - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); - return; - } - } -}; - -m.DefenseManager.prototype.Serialize = function() -{ - let properties = { - "targetList": this.targetList, - "armyMergeSize": this.armyMergeSize, - "attackingUnits": this.attackingUnits, - "attackingArmies": this.attackingArmies, - "attackedAllies": this.attackedAllies - }; - - let armies = []; - for (let army of this.armies) - armies.push(army.Serialize()); - - return { "properties": properties, "armies": armies }; -}; - -m.DefenseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.armies = []; - for (let dataArmy of data.armies) - { - let army = new m.DefenseArmy(gameState, []); - army.Deserialize(dataArmy); - this.armies.push(army); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/diplomacyManager.js b/install/petraBased/petra-single-base/diplomacyManager.js deleted file mode 100644 index 5b8e259..0000000 --- a/install/petraBased/petra-single-base/diplomacyManager.js +++ /dev/null @@ -1,559 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the diplomacy: - * update our cooperative trait - * sent tribute to allies - * decide which player to turn against in "Last Man Standing" mode - * respond to diplomacy requests - * send diplomacy requests to other players (rarely) - */ - -/** - * If a player sends us an ally or neutral request, an Object in this.receivedDiplomacyRequests will be created - * that includes the request status, and the amount and type of the resource tribute (if any) - * that they must send in order for us to accept their request. - * In addition, a message will be sent if the player has not sent us a tribute within a minute. - * If two minutes pass without a tribute, we will decline their request. - * - * If we send a diplomacy request to another player, an Object in this.sentDiplomacyRequests will be created, - * which consists of the requestType (i.e. "ally" or "neutral") and the timeSent. A chat message will be sent - * to the other player, and AI players will actually be informed of the request by a DiplomacyRequest event - * sent through AIInterface. It is expected that the other player will change their diplomacy stance to the stance - * that we suggested within a period of time, or else the request will be deleted from this.sentDiplomacyRequests. - */ -m.DiplomacyManager = function(Config) -{ - 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 = 120 + randFloat(10, 100); -}; - -/** - * 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. - */ -m.DiplomacyManager.prototype.init = function(gameState) -{ - this.lastManStandingCheck(gameState); - - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - 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 - */ -m.DiplomacyManager.prototype.tributes = function(gameState) -{ - this.nextTributeUpdate = gameState.ai.elapsedTime + 30; - let totalResources = gameState.getResources(); - let 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; - let donor = gameState.getAlliedVictory() || gameState.getEntities(i).length < gameState.getOwnEntities().length; - let allyResources = gameState.sharedScript.playersData[i].resourceCounts; - let allyPop = gameState.sharedScript.playersData[i].popCount; - let tribute = {}; - let toSend = false; - for (let res in allyResources) - { - 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); - for (let k = 0; k < 2; ++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); - m.chatRequestTribute(gameState, res); - if (this.Config.debug > 1) - API3.warn("Tribute on " + res + " requested to player " + i); - break; - } - } - } - } - if (!toSend) - continue; - if (this.Config.debug > 1) - API3.warn("Tribute " + uneval(tribute) + " sent to player " + i); - if (this.Config.chat) - m.chatSentTribute(gameState, i); - Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute }); - } -}; - -m.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 (let evt of events.TributeExchanged) - { - if (evt.to === PlayerID && !gameState.isPlayerAlly(evt.from) && this.receivedDiplomacyRequests.has(evt.from)) - { - let request = this.receivedDiplomacyRequests.get(evt.from); - if (request.status === "waitingForTribute") - { - request.wanted -= evt.amounts[request.type]; - - if (request.wanted <= 0) - { - if (this.Config.debug > 1) - API3.warn("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 (let 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 (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || - gameState.ai.HQ.territoryMap.getOwner(target.position()) !== PlayerID || - !gameState.isPlayerEnemy(target.owner())) - continue; - let 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 (let 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 - { - let 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; - } - - let 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)) - { - let response = request !== undefined && (request.status === "declinedRequest" || request.status === "allianceBroken") ? - "decline" : "declineSuggestNeutral"; - m.chatAnswerRequestDiplomacy(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 (let evt of events.DiplomacyRequest) - { - if (evt.player !== PlayerID) - continue; - - this.handleDiplomacyRequest(gameState, evt.source, evt.to); - let request = this.receivedDiplomacyRequests.get(evt.source); - if (this.Config.debug > 0) - API3.warn("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 - }); - } - } - - // An AI player we sent a diplomacy request to demanded we send them a tribute - for (let evt of events.TributeRequest) - { - if (evt.player !== PlayerID) - continue; - - let 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 responseTribute = {}; - responseTribute[evt.resourceType] = evt.resourceWanted; - if (this.Config.debug > 0) - API3.warn("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; - } - } -}; - -/** - * 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. - */ -m.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 turnFactor = 0; - 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; - - 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")) - { - let wonder = gameState.getEnemyStructures(i).filter(API3.Filters.byClass("Wonder"))[0]; - if (wonder) - { - let wonderProgess = wonder.foundationProgress(); - if (wonderProgess === undefined) - { - playerToTurnAgainst = i; - break; - } - turnFactor += wonderProgess * 2.5 + this.betrayWeighting; - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - let relicsCount = gameState.updatingGlobalCollection("allRelics", API3.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"); - let request = this.receivedDiplomacyRequests.get(playerToTurnAgainst); - if (request && request.status !== "allianceBroken") - { - if (request.status === "waitingForTribute") - m.chatAnswerRequestDiplomacy(gameState, player, 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. - */ -m.DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player, requestType) -{ - if (gameState.sharedScript.playersData[PlayerID].teamsLocked) - return; - let response; - let requiredTribute; - let request = this.receivedDiplomacyRequests.get(player); - let 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"; - } - 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; - } - } - 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 - { - response = "acceptWithTribute"; - requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState)[0]; - 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 - }); - } - m.chatAnswerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); -}; - -m.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) - API3.warn("diplomacy stance with player " + player + " is now " + newDiplomaticStance); - if (this.Config.chat) - m.chatNewDiplomacy(gameState, player, newDiplomaticStance); -}; - -m.DiplomacyManager.prototype.checkRequestedTributes = function(gameState) -{ - for (let [player, data] of this.receivedDiplomacyRequests) - if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime) - { - if (data.sentWarning) - { - this.receivedDiplomacyRequests.delete(player); - m.chatAnswerRequestDiplomacy(gameState, player, data.requestType, "decline"); - } - else - { - data.sentWarning = true; - data.warnTime = gameState.ai.elapsedTime + 60; - m.chatAnswerRequestDiplomacy(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. - */ -m.DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState) -{ - let player; - let max = 0; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - let mutualEnemies = 0; - let 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; - - 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; - - let requestType = gameState.isPlayerNeutral(player) ? "ally" : "neutral"; - - this.sentDiplomacyRequests.set(player, { - "requestType": requestType, - "timeSent": gameState.ai.elapsedTime - }); - - if (this.Config.debug > 0) - API3.warn("Sending diplomacy request to player " + player + " with " + requestType); - Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType }); - m.chatNewRequestDiplomacy(gameState, player, requestType, "sendRequest"); -}; - -m.DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState) -{ - for (let [player, data] of this.sentDiplomacyRequests) - if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources && - gameState.getPopulation() > 70) - { - m.chatNewRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); - this.sentDiplomacyRequests.delete(player); - } -}; - -m.DiplomacyManager.prototype.update = function(gameState, events) -{ - this.checkEvents(gameState, events); - - if (!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) - { - this.sentDiplomacyRequestLapseTime = gameState.ai.elapsedTime + 300 + randFloat(10, 100); - let 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); -}; - -m.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 - }; -}; - -m.DiplomacyManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/entityExtend.js b/install/petraBased/petra-single-base/entityExtend.js deleted file mode 100644 index 3edbdbf..0000000 --- a/install/petraBased/petra-single-base/entityExtend.js +++ /dev/null @@ -1,441 +0,0 @@ -var PETRA = function(m) -{ - -/** returns true if this unit should be considered as a siege unit */ -m.isSiegeUnit = function(ent) -{ - return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee") && ent.hasClass("Champion"); -}; - -/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ -m.getMaxStrength = function(ent, againstClass) -{ - let strength = 0; - let attackTypes = ent.attackTypes(); - if (!attackTypes) - return strength; - - for (let type of attackTypes) - { - if (type == "Slaughter") - continue; - - let attackStrength = ent.attackStrengths(type); - for (let str in attackStrength) - { - let val = parseFloat(attackStrength[str]); - if (againstClass) - val *= ent.getMultiplierAgainst(type, againstClass); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength"); - } - } - - let attackRange = ent.attackRange(type); - if (attackRange) - strength += attackRange.max * 0.0125; - - let attackTimes = ent.attackTimes(type); - for (let str in attackTimes) - { - let val = parseFloat(attackTimes[str]); - switch (str) - { - case "repeat": - strength += val / 100000; - break; - case "prepare": - strength -= val / 100000; - break; - default: - API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); - } - } - } - - let armourStrength = ent.armourStrengths(); - for (let str in armourStrength) - { - let val = parseFloat(armourStrength[str]); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength"); - } - } - - return strength * ent.maxHitpoints() / 100.0; -}; - -/** Get access and cache it (except for units as it can change) in metadata if not already done */ -m.getLandAccess = function(gameState, ent) -{ - if (ent.hasClass("Unit")) - return gameState.ai.accessibility.getAccessValue(ent.position()); - - let access = ent.getMetadata(PlayerID, "access"); - if (!access) - { - access = gameState.ai.accessibility.getAccessValue(ent.position()); - // Docks are sometimes not as expected - if (access < 2 && ent.buildPlacementType() == "shore") - { - let halfDepth = 0; - if (ent.get("Footprint/Square")) - halfDepth = +ent.get("Footprint/Square/@depth") / 2; - else if (ent.get("Footprint/Circle")) - halfDepth = +ent.get("Footprint/Circle/@radius"); - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < halfDepth; d += 3) - { - let pos = [ entPos[0] - d * sina, - entPos[1] - d * cosa]; - access = gameState.ai.accessibility.getAccessValue(pos); - if (access > 1) - break; - } - } - ent.setMetadata(PlayerID, "access", access); - } - return access; -}; - -/** Sea access always cached as it never changes */ -m.getSeaAccess = function(gameState, ent) -{ - let sea = ent.getMetadata(PlayerID, "sea"); - if (!sea) - { - sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); - // Docks are sometimes not as expected - if (sea < 2 && ent.buildPlacementType() == "shore") - { - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < 15; d += 3) - { - let pos = [ entPos[0] + d * sina, - entPos[1] + d * cosa]; - sea = gameState.ai.accessibility.getAccessValue(pos, true); - if (sea > 1) - break; - } - } - ent.setMetadata(PlayerID, "sea", sea); - } - return sea; -}; - -m.setSeaAccess = function(gameState, ent) -{ - m.getSeaAccess(gameState, ent); -}; - -/** Decide if we should try to capture (returns true) or destroy (return false) */ -m.allowCapture = function(gameState, ent, target) -{ - if (!target.isCapturable() || !ent.canCapture(target)) - return false; - if (target.isInvulnerable()) - return true; - // always try to recapture cp from an allied, except if it's decaying - if (gameState.isPlayerAlly(target.owner())) - return !target.decaying(); - - let antiCapture = target.defaultRegenRate(); - if (target.isGarrisonHolder() && target.garrisoned()) - antiCapture += target.garrisonRegenRate() * target.garrisoned().length; - if (target.decaying()) - antiCapture -= target.territoryDecayRate(); - - let capture; - let capturableTargets = gameState.ai.HQ.capturableTargets; - if (!capturableTargets.has(target.id())) - { - capture = ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); - } - else - { - let capturable = capturableTargets.get(target.id()); - if (!capturable.ents.has(ent.id())) - { - capturable.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturable.ents.add(ent.id()); - } - capture = capturable.strength; - } - capture *= 1 / (0.1 + 0.9*target.healthLevel()); - let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); - if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) - return capture > antiCapture + sumCapturePoints/50; - return capture > antiCapture + sumCapturePoints/80; -}; - -m.getAttackBonus = function(ent, target, type) -{ - let attackBonus = 1; - if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) - return attackBonus; - let bonuses = ent.get("Attack/" + type + "/Bonuses"); - for (let key in bonuses) - { - let bonus = bonuses[key]; - if (bonus.Civ && bonus.Civ !== target.civ()) - continue; - if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) - continue; - attackBonus *= bonus.Multiplier; - } - return attackBonus; -}; - -/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ -m.returnResources = function(gameState, ent) -{ - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) - return false; - - let resource = ent.resourceCarrying()[0].type; - - let closestDropsite; - let distmin = Math.min(); - let access = m.getLandAccess(gameState, ent); - let dropsiteCollection = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); - for (let dropsite of dropsiteCollection.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again - if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - closestDropsite = dropsite; - } - - if (!closestDropsite) - return false; - ent.returnResources(closestDropsite); - return true; -}; - -/** is supply full taking into account gatherers affected during this turn */ -m.IsSupplyFull = function(gameState, ent) -{ - return ent.isFull() === true || - ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); -}; - -/** - * Get the best base (in terms of distance and accessIndex) for an entity. - * It should be on the same accessIndex for structures. - * If nothing found, return the base[0] for units and undefined for structures. - * If exclude is given, we exclude the base with ID = exclude. - */ -m.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) -{ - let pos = ent.position(); - let accessIndex; - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - { - API3.warn("Petra error: entity without position, but not garrisoned"); - m.dumpEntity(ent); - return gameState.ai.HQ.baseManagers[0]; - } - pos = holder.position(); - accessIndex = m.getLandAccess(gameState, holder); - } - else - accessIndex = m.getLandAccess(gameState, ent); - - let distmin = Math.min(); - let dist; - let bestbase; - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) - continue; - if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) - continue; - if (ent.hasClass("Structure") && base.accessIndex != accessIndex) - continue; - if (base.anchor && base.anchor.position()) - dist = API3.SquareVectorDistance(base.anchor.position(), pos); - else - { - let found = false; - for (let structure of base.buildings.values()) - { - if (!structure.position()) - continue; - dist = API3.SquareVectorDistance(structure.position(), pos); - found = true; - break; - } - if (!found) - continue; - } - if (base.accessIndex != accessIndex) - dist += 50000000; - if (!base.anchor) - dist += 50000000; - if (dist > distmin) - continue; - distmin = dist; - bestbase = base; - } - if (!bestbase && !ent.hasClass("Structure")) - bestbase = gameState.ai.HQ.baseManagers[0]; - return bestbase; -}; - -m.getHolder = function(gameState, ent) -{ - for (let holder of gameState.getEntities().values()) - { - if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) - return holder; - } - return undefined; -}; - -/** return the template of the built foundation if a foundation, otherwise return the entity itself */ -m.getBuiltEntity = function(gameState, ent) -{ - if (ent.foundationProgress() !== undefined) - return gameState.getBuiltTemplate(ent.templateName()); - - return ent; -}; - -/** - * return true if it is not worth finishing this building (it would surely decay) - * TODO implement the other conditions - */ -m.isNotWorthBuilding = function(gameState, ent) -{ - if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) - { - let buildTerritories = ent.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) - return true; - } - return false; -}; - -/** - * Check if the straight line between the two positions crosses an enemy territory - */ -m.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) -{ - let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; - let stepx = (pos2[0] - pos1[0]) / n; - let stepy = (pos2[1] - pos1[1]) / n; - for (let i = 1; i < n; ++i) - { - let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; - let owner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (owner && gameState.isPlayerEnemy(owner)) - return true; - } - return false; -}; - -m.gatherTreasure = function(gameState, ent, water = false) -{ - if (!gameState.ai.HQ.treasures.hasEntities()) - return false; - if (!ent || !ent.position()) - return false; - let rates = ent.resourceGatherRates(); - if (!rates || !rates.treasure || rates.treasure <= 0) - return false; - let treasureFound; - let distmin = Math.min(); - let access = water ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - for (let treasure of gameState.ai.HQ.treasures.values()) - { - if (m.IsSupplyFull(gameState, treasure)) - continue; - // let some time for the previous gatherer to reach the treasure before trying again - let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); - if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) - continue; - if (!water && access != m.getLandAccess(gameState, treasure)) - continue; - if (water && access != m.getSeaAccess(gameState, treasure)) - continue; - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - continue; - let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); - if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit - continue; - if (dist > distmin) - continue; - distmin = dist; - treasureFound = treasure; - } - if (!treasureFound) - return false; - treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); - ent.gather(treasureFound); - gameState.ai.HQ.AddTCGatherer(treasureFound.id()); - ent.setMetadata(PlayerID, "supply", treasureFound.id()); - return true; -}; - -m.dumpEntity = function(ent) -{ - if (!ent) - return; - API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + - " state " + ent.unitAIState()); - API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + - " subrole " + ent.getMetadata(PlayerID, "subrole")); - API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + - " foundationProgress " + ent.foundationProgress()); - API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + - " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + - " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); - API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + - " gather-type " + ent.getMetadata(PlayerID, "gather-type") + - " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + - " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/garrisonManager.js b/install/petraBased/petra-single-base/garrisonManager.js deleted file mode 100644 index c9ec57c..0000000 --- a/install/petraBased/petra-single-base/garrisonManager.js +++ /dev/null @@ -1,373 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the garrisonHolders - * When a unit is ordered to garrison, it must be done through this.garrison() function so that - * an object in this.holders is created. This object contains an array with the entities - * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). - * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) - */ - -m.GarrisonManager = function(Config) -{ - this.Config = Config; - this.holders = new Map(); - this.decayingStructures = new Map(); -}; - -m.GarrisonManager.prototype.update = function(gameState, events) -{ - // First check for possible upgrade of a structure - for (let evt of events.EntityRenamed) - { - for (let id of this.holders.keys()) - { - if (id != evt.entity) - continue; - let data = this.holders.get(id); - let newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) - { - this.holders.delete(id); - this.holders.set(evt.newentity, data); - } - else - { - for (let entId of data.list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - continue; - this.leaveGarrison(ent); - ent.stopMoving(); - } - this.holders.delete(id); - } - } - - for (let id of this.decayingStructures.keys()) - { - if (id !== evt.entity) - continue; - this.decayingStructures.delete(id); - if (this.decayingStructures.has(evt.newentity)) - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) - continue; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(evt.newentity, gmin); - } - } - - for (let [id, data] of this.holders.entries()) - { - let list = data.list; - let holder = gameState.getEntityById(id); - if (!holder || !gameState.isPlayerAlly(holder.owner())) - { - // this holder was certainly destroyed or captured. Let's remove it - for (let entId of list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - 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 (let evt of events.EntityRenamed) - if (evt.entity === list[j]) - list[j] = evt.newentity; - - let ent = gameState.getEntityById(list[j]); - if (!ent) // unit must have been killed while garrisoning - list.splice(j--, 1); - else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned - { - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (ent.unitAIOrderData().some(order => order.target && order.target == id)) - continue; - if (ent.getMetadata(PlayerID, "garrisonHolder") == id) - { - // The garrison order must have failed - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (gameState.ai.Config.debug > 0) - { - API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + - ") is expected to garrison in " + id + " (" + holder.genericName() + - "), but has no such garrison order " + uneval(ent.unitAIOrderData())); - m.dumpEntity(ent); - } - list.splice(j--, 1); - } - } - - } - - if (!holder.position()) // could happen with siege unit inside a ship - continue; - - if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) - { - let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; - let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; - for (let 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; - let dist = API3.SquareVectorDistance(ent.position(), holder.position()); - if (dist > range*range) - continue; - if (ent.hasClass("Structure")) - around.defenseStructure = true; - else if (m.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 (let entId of holder.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) - holder.unload(entId); - } - for (let j = 0; j < list.length; ++j) - { - let 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.numberOfGarrisonedUnits(holder) === 0) - this.holders.delete(id); - else - holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); - } - } - - // Warning new garrison orders (as in the following lines) should be done after having updated the holders - // (or TODO we should add a test that the garrison order is from a previous turn when updating) - for (let [id, gmin] of this.decayingStructures.entries()) - { - let ent = gameState.getEntityById(id); - if (!ent || ent.owner() !== PlayerID) - this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedUnits(ent) < gmin) - gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); - } -}; - -/** TODO should add the units garrisoned inside garrisoned units */ -m.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) -{ - if (!this.holders.has(holder.id())) - return holder.garrisoned().length; - - return holder.garrisoned().length + this.holders.get(holder.id()).list.length; -}; - -m.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 */ -m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) -{ - if (this.numberOfGarrisonedUnits(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")); - } - - if (ent.getMetadata(PlayerID, "plan") !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - ent.setMetadata(PlayerID, "subrole", "garrisoning"); - ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); - ent.setMetadata(PlayerID, "garrisonType", type); - ent.garrison(holder); -}; - -/** - This is the end of the pre-garrison state, either because the entity is really garrisoned - or because it has changed its order (i.e. because the garrisonHolder was destroyed) - 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 - */ -m.GarrisonManager.prototype.leaveGarrison = function(ent) -{ - ent.setMetadata(PlayerID, "subrole", undefined); - if (ent.getMetadata(PlayerID, "plan") === -2) - ent.setMetadata(PlayerID, "plan", -1); - else - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "garrisonHolder", undefined); -}; - -/** Cancel a pre-garrison state */ -m.GarrisonManager.prototype.cancelGarrison = function(ent) -{ - ent.stopMoving(); - this.leaveGarrison(ent); - let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); - if (!holderId || !this.holders.has(holderId)) - return; - let list = this.holders.get(holderId).list; - let index = list.indexOf(ent.id()); - if (index !== -1) - list.splice(index, 1); -}; - -m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) -{ - switch (ent.getMetadata(PlayerID, "garrisonType")) - { - case 'force': // force the ungarrisoning - return false; - case 'trade': // trader garrisoned in ship - return true; - case 'protection': // hurt unit for healing or infantry for defense - if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - let capture = ent.capturePoints(); - if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) - return true; - if (MatchesClassList(ent.classes(), 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") || m.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above - if (m.isSiegeUnit(ent)) - return around.meleeSiege; - return holder.buffHeal() && ent.needsHeal(); - case 'decay': - return this.decayingStructures.has(holder.id()); - case '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; - API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + - " for " + ent.genericName() + " id " + ent.id() + - " inside " + holder.genericName() + " id " + holder.id()); - ent.setMetadata(PlayerID, "garrisonType", "protection"); - return true; - } -}; - -/** Add this holder in the list managed by the garrisonManager */ -m.GarrisonManager.prototype.registerHolder = function(gameState, holder) -{ - if (this.holders.has(holder.id())) // already registered - return; - this.holders.set(holder.id(), { "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) - */ -m.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) -{ - if (this.decayingStructures.has(entId)) - return true; - let ent = gameState.getEntityById(entId); - if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) - return false; - if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) - return false; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(entId, gmin); - return true; -}; - -m.GarrisonManager.prototype.removeDecayingStructure = function(entId) -{ - if (!this.decayingStructures.has(entId)) - return; - this.decayingStructures.delete(entId); -}; - -m.GarrisonManager.prototype.Serialize = function() -{ - return { "holders": this.holders, "decayingStructures": this.decayingStructures }; -}; - -m.GarrisonManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/mapModule.js b/install/petraBased/petra-single-base/mapModule.js deleted file mode 100644 index 225767a..0000000 --- a/install/petraBased/petra-single-base/mapModule.js +++ /dev/null @@ -1,221 +0,0 @@ -var PETRA = function(m) -{ - -/** map functions */ - -m.TERRITORY_PLAYER_MASK = 0x1F; -m.TERRITORY_BLINKING_MASK = 0x40; - -m.createObstructionMap = function(gameState, accessIndex, template) -{ - let passabilityMap = gameState.getPassabilityMap(); - let territoryMap = gameState.ai.territoryMap; - let ratio = territoryMap.cellSize / passabilityMap.cellSize; - - // default values - let placementType = "land"; - let buildOwn = true; - let buildAlly = true; - let buildNeutral = true; - let buildEnemy = false; - // If there is a template then replace the defaults - if (template) - { - placementType = template.buildPlacementType(); - buildOwn = template.hasBuildTerritory("own"); - buildAlly = template.hasBuildTerritory("ally"); - buildNeutral = template.hasBuildTerritory("neutral"); - buildEnemy = template.hasBuildTerritory("enemy"); - } - let obstructionTiles = new Uint8Array(passabilityMap.data.length); - - let passMap; - let obstructionMask; - if (placementType == "shore") - { - passMap = gameState.ai.accessibility.navalPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-shore"); - } - else - { - passMap = gameState.ai.accessibility.landPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-land"); - } - - for (let k = 0; k < territoryMap.data.length; ++k) - { - let tilePlayer = territoryMap.data[k] & m.TERRITORY_PLAYER_MASK; - let isConnected = (territoryMap.data[k] & m.TERRITORY_BLINKING_MASK) == 0; - if (tilePlayer === PlayerID) - { - if (!buildOwn || !buildNeutral && !isConnected) - continue; - } - else if (gameState.isPlayerMutualAlly(tilePlayer)) - { - if (!buildAlly || !buildNeutral && !isConnected) - continue; - } - else if (tilePlayer === 0) - { - if (!buildNeutral) - continue; - } - else - { - if (!buildEnemy) - continue; - } - - let x = ratio * (k % territoryMap.width); - let y = ratio * Math.floor(k / territoryMap.width); - for (let ix = 0; ix < ratio; ++ix) - { - for (let iy = 0; iy < ratio; ++iy) - { - let i = x + ix + (y + iy)*passabilityMap.width; - if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) - continue; - if (!(passabilityMap.data[i] & obstructionMask)) - obstructionTiles[i] = 255; - } - } - } - - let map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); - map.setMaxVal(255); - - if (template && template.buildDistance()) - { - let distance = template.buildDistance(); - let minDist = distance.MinDistance ? +distance.MinDistance : 0; - if (minDist) - { - let obstructionRadius = template.obstructionRadius(); - if (obstructionRadius) - minDist -= obstructionRadius.min; - let fromClass = distance.FromClass; - let cellSize = passabilityMap.cellSize; - let cellDist = 1 + minDist / cellSize; - let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); - for (let ent of structures.values()) - { - if (!ent.position()) - continue; - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - map.addInfluence(x, z, cellDist, -255, "constant"); - } - } - } - - return map; -}; - - -m.createTerritoryMap = function(gameState) -{ - let map = gameState.ai.territoryMap; - - let ret = new API3.Map(gameState.sharedScript, "territory", map.data); - ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; - ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; - ret.isBlinking = function(p) { return (this.point(p) & m.TERRITORY_BLINKING_MASK) != 0; }; - return ret; -}; - -/** - * The borderMap contains some border and frontier information: - * - border of the map filled once: - * - all mini-cells (1x1) from the big cell (8x8) inaccessibles => bit 0 - * - inside a given distance to the border => bit 1 - * - frontier of our territory (updated regularly in updateFrontierMap) - * - narrow border (inside our territory) => bit 2 - * - large border (inside our territory, exclusive of narrow) => bit 3 - */ - -m.outside_Mask = 1; -m.border_Mask = 2; -m.fullBorder_Mask = m.outside_Mask | m.border_Mask; -m.narrowFrontier_Mask = 4; -m.largeFrontier_Mask = 8; -m.fullFrontier_Mask = m.narrowFrontier_Mask | m.largeFrontier_Mask; - -m.createBorderMap = function(gameState) -{ - let map = new API3.Map(gameState.sharedScript, "territory"); - let width = map.width; - let border = Math.round(80 / map.cellSize); - let passabilityMap = gameState.getPassabilityMap(); - let obstructionMask = gameState.getPassabilityClassMask("unrestricted"); - if (gameState.circularMap) - { - let ic = (width - 1) / 2; - let radcut = (ic - border) * (ic - border); - for (let j = 0; j < map.length; ++j) - { - let dx = j%width - ic; - let dy = Math.floor(j/width) - ic; - let radius = dx*dx + dy*dy; - if (radius < radcut) - continue; - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - else - { - let borderCut = width - border; - for (let j = 0; j < map.length; ++j) - { - let ix = j%width; - let iy = Math.floor(j/width); - if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) - { - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - } - -// map.dumpIm("border.png", 5); - return map; -}; - -m.debugMap = function(gameState, map) -{ - let width = map.width; - let cell = map.cellSize; - gameState.getEntities().forEach(ent => { - let pos = ent.position(); - if (!pos) - return; - let x = Math.round(pos[0] / cell); - let z = Math.round(pos[1] / cell); - let id = x + width*z; - if (map.map[id] == 1) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else if (map.map[id] == 2) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - else if (map.map[id] == 3) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 0, 2] }); - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/navalManager.js b/install/petraBased/petra-single-base/navalManager.js deleted file mode 100644 index 5a16d0f..0000000 --- a/install/petraBased/petra-single-base/navalManager.js +++ /dev/null @@ -1,896 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Naval Manager - * Will deal with anything ships. - * -Basically trade over water (with fleets and goals commissioned by the economy manager) - * -Defense over water (commissioned by the defense manager) - * -Transport of units over water (a few units). - * -Scouting, ultimately. - * Also deals with handling docks, making sure we have access and stuffs like that. - */ - -m.NavalManager = function(Config) -{ - this.Config = Config; - - // ship subCollections. Also exist for land zones, idem, not caring. - this.seaShips = []; - this.seaTransportShips = []; - this.seaWarShips = []; - this.seaFishShips = []; - - // wanted NB per zone. - this.wantedTransportShips = []; - this.wantedWarShips = []; - this.wantedFishShips = []; - // needed NB per zone. - this.neededTransportShips = []; - this.neededWarShips = []; - - this.transportPlans = []; - - // shore-line regions where we can load and unload units - this.landingZones = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.NavalManager.prototype.init = function(gameState, deserializing) -{ - // docks - this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"])); - this.docks.registerUpdates(); - - this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); - // note: those two can overlap (some transport ships are warships too and vice-versa). - this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); - this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); - this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); - - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - this.warShips.registerUpdates(); - this.fishShips.registerUpdates(); - - let availableFishes = {}; - for (let fish of gameState.getFishableSupplies().values()) - { - let sea = this.getFishSea(gameState, fish); - if (sea && availableFishes[sea]) - availableFishes[sea] += fish.resourceSupplyAmount(); - else if (sea) - availableFishes[sea] = fish.resourceSupplyAmount(); - } - - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (!gameState.ai.HQ.navalRegions[i]) - { - // push dummies - this.seaShips.push(undefined); - this.seaTransportShips.push(undefined); - this.seaWarShips.push(undefined); - this.seaFishShips.push(undefined); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - else - { - let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaShips.push(collec); - collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaTransportShips.push(collec); - collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaWarShips.push(collec); - collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaFishShips.push(collec); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - if (availableFishes[i] && availableFishes[i] > 1000) - this.wantedFishShips.push(this.Config.Economy.targetNumFishers); - else - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - } - - if (deserializing) - return; - - // determination of the possible landing zones - let width = gameState.getPassabilityMap().width; - let length = width * gameState.getPassabilityMap().height; - for (let i = 0; i < length; ++i) - { - let land = gameState.ai.accessibility.landPassMap[i]; - if (land < 2) - continue; - let naval = gameState.ai.accessibility.navalPassMap[i]; - if (naval < 2) - continue; - if (!this.landingZones[land]) - this.landingZones[land] = {}; - if (!this.landingZones[land][naval]) - this.landingZones[land][naval] = new Set(); - this.landingZones[land][naval].add(i); - } - // and keep only thoses with enough room around when possible - for (let land in this.landingZones) - { - for (let sea in this.landingZones[land]) - { - let landing = this.landingZones[land][sea]; - let nbaround = {}; - let nbcut = 0; - for (let i of landing) - { - let nb = 0; - if (landing.has(i-1)) - nb++; - if (landing.has(i+1)) - nb++; - if (landing.has(i+width)) - nb++; - if (landing.has(i-width)) - nb++; - nbaround[i] = nb; - nbcut = Math.max(nb, nbcut); - } - nbcut = Math.min(2, nbcut); - for (let i of landing) - { - if (nbaround[i] < nbcut) - landing.delete(i); - } - } - } - - // Assign our initial docks and ships - for (let ship of this.ships.values()) - m.setSeaAccess(gameState, ship); - for (let dock of this.docks.values()) - m.setSeaAccess(gameState, dock); -}; - -m.NavalManager.prototype.updateFishingBoats = function(sea, num) -{ - if (this.wantedFishShips[sea]) - this.wantedFishShips[sea] = num; -}; - -m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) -{ - if (sea !== undefined) - this.wantedFishShips[sea] = 0; - else - this.wantedFishShips.fill(0); -}; - -/** Get the sea, cache it if not yet done and check if in opensea */ -m.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]]; - let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); - let width = gameState.ai.accessibility.width; - let k = pos[0] + pos[1]*width; - sea = gameState.ai.accessibility.navalPassMap[k]; - fish.setMetadata(PlayerID, "sea", sea); - let radius = 120 / gameState.ai.accessibility.cellSize / ntry; - if (around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let 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) - { - let 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 */ -m.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]]; - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let radius = 120 / territoryMap.cellSize / ntry; - let pos = territoryMap.gamePosToMapPos(fish.position()); - return around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); - if (i < 0 || i >= width || j < 0 || j >= width) - continue; - let 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 */ -m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) -{ - let seas = gameState.ai.accessibility.regionLinks[region].slice(); - this.docks.forEach(dock => { - if (!dock.hasClass("Dock") || m.getLandAccess(gameState, dock) != region) - return; - let i = seas.indexOf(m.getSeaAccess(gameState, dock)); - if (i != -1) - seas.splice(i--, 1); - }); - return seas; -}; - -m.NavalManager.prototype.checkEvents = function(gameState, queues, events) -{ - for (let evt of events.Create) - { - if (!evt.entity) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } - - for (let evt of events.TrainingFinished) - { - if (!evt.entities) - continue; - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) - continue; - m.setSeaAccess(gameState, ent); - } - } - - for (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) - continue; - if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) - continue; - let plan = this.getPlan(evt.metadata[PlayerID].transporter); - if (!plan) - continue; - - let shipId = evt.entityObj.id(); - if (this.Config.debug > 1) - API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); - if (plan.state == "boarding") - { - // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan - plan.units.forEach(ent => { - if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() || - ent.getMetadata(PlayerID, "onBoard") == shipId) - ent.setMetadata(PlayerID, "onBoard", undefined); - }); - plan.needTransportShips = !plan.transportShips.hasEntities(); - } - else if (plan.state == "sailing") - { - let endIndex = plan.endIndex; - for (let ent of plan.units.values()) - { - if (!ent.position()) // unit from another ship of this plan ... do nothing - continue; - let access = m.getLandAccess(gameState, ent); - let endPos = ent.getMetadata(PlayerID, "endPos"); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // nothing else to do if access = endIndex as already at destination - // otherwise, we should require another transport - // TODO if attacking and no more ships available, remove the units from the attack - // to avoid delaying it too much - if (access != endIndex) - this.requireTransport(gameState, ent, access, endIndex, endPos); - } - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.to !== PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } -}; - - -m.NavalManager.prototype.getPlan = function(ID) -{ - for (let plan of this.transportPlans) - if (plan.ID === ID) - return plan; - return undefined; -}; - -m.NavalManager.prototype.addPlan = function(plan) -{ - this.transportPlans.push(plan); -}; - -/** - * complete already existing plan or create a new one for this requirement - * (many units can then call this separately and end up in the same plan) - * TODO check garrison classes - */ -m.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) -{ - if (!ent.canGarrison()) - return false; - - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - if (this.Config.debug > 0) - API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport"); - return false; - } - - let plans = []; - for (let plan of this.transportPlans) - { - if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding") - continue; - // Limit the number of siege units per transport to avoid problems when ungarrisoning - if (m.isSiegeUnit(ent) && plan.units.filter(unit => m.isSiegeUnit(unit)).length > 3) - continue; - plans.push(plan); - } - - if (plans.length) - { - plans.sort(plan => plan.units.length); - plans[0].addUnit(ent, endPos); - return true; - } - - let plan = new m.TransportPlan(gameState, [ent], startIndex, endIndex, endPos); - if (plan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> transport plan aborted <<<<"); - return false; - } - plan.init(gameState); - this.transportPlans.push(plan); - return true; -}; - -/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ -m.NavalManager.prototype.splitTransport = function(gameState, plan) -{ - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan started <<<<"); - let newplan = new m.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); - if (newplan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan aborted <<<<"); - return false; - } - newplan.init(gameState); - - for (let 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 - */ -m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) -{ - let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); - if (fromAccess !== 1) - return; - if (toAccess < 2) - return; - - for (let ship of this.ships.values()) - { - if (!ship.isGarrisonHolder() || !ship.garrisoned().length) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined) - continue; - let units = []; - for (let entId of ship.garrisoned()) - units.push(gameState.getEntityById(entId)); - // TODO check that the garrisoned units have not another purpose - let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); - if (plan.failed) - continue; - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - -// set minimal number of needed ships when a new event (new base or new attack plan) -m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) -{ - if (!sea) - return; - if (this.wantedTransportShips[sea] < number) - this.wantedTransportShips[sea] = number; -}; - -// bumps up the number of ships we want if we need more. -m.NavalManager.prototype.checkLevels = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - this.neededTransportShips[sea] = 0; - - for (let plan of this.transportPlans) - { - if (!plan.needTransportShips || plan.units.length < 2) - continue; - let sea = plan.sea; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || - this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - continue; - ++this.neededTransportShips[sea]; - if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) - { - ++this.wantedTransportShips[sea]; - return; - } - } - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - if (this.neededTransportShips[sea] > 2) - ++this.wantedTransportShips[sea]; -}; - -m.NavalManager.prototype.maintainFleet = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) - return; - // check if we have enough transport ships per region. - for (let sea = 0; sea < this.seaShips.length; ++sea) - { - if (this.seaShips[sea] === undefined) - continue; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) - continue; - - if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - { - let template = this.getBestShip(gameState, sea, "transport"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); - continue; - } - } - - - if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) - { - let template = this.getBestShip(gameState, sea, "fishing"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); - continue; - } - } - } -}; - -/** assigns free ships to plans that need some */ -m.NavalManager.prototype.assignShipsToPlans = function(gameState) -{ - for (let plan of this.transportPlans) - if (plan.needTransportShips) - plan.assignShip(gameState); -}; - -/** Return true if this ship is likeky (un)garrisoning units */ -m.NavalManager.prototype.isShipBoarding = function(ship) -{ - if (!ship.position()) - return false; - let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); - if (!plan || !plan.boardingPos[ship.id()]) - return false; - return API3.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. - */ -m.NavalManager.prototype.moveApart = function(gameState) -{ - let blockedShips = []; - let blockedIds = []; - - for (let ship of this.ships.values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) - continue; - - let unitAIState = ship.unitAIState(); - if (ship.getMetadata(PlayerID, "transporter") !== undefined || - unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - - } - } - - for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - let role = ship.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - } - } - - for (let ship of blockedShips) - { - let shipPosition = ship.position(); - let sea = ship.getMetadata(PlayerID, "sea"); - for (let blockingShip of this.seaShips[sea].values()) - { - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && - unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && - unitAIState != "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - - for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - if (blockingShip.getMetadata(PlayerID, "sea") != sea) - continue; - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let role = blockingShip.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - } -}; - -m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) -{ - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) - return; - - if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) - { - if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && - !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock")) - { - let dockStarted = false; - for (let base of gameState.ai.HQ.baseManagers) - { - if (dockStarted) - break; - if (!base.anchor || base.constructing) - continue; - let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); - for (let sea of remaining) - { - if (!gameState.ai.HQ.navalRegions[sea]) - continue; - let wantedLand = {}; - wantedLand[base.accessIndex] = true; - queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); - dockStarted = true; - break; - } - } - } - } - - if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || - queues.militaryBuilding.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || - this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) - return; - // Use in priority resources to build a market - if (!gameState.getOwnEntitiesByClass("BarterMarket", 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; - let wantedLand = {}; - for (let base of gameState.ai.HQ.baseManagers) - if (base.anchor) - wantedLand[base.accessIndex] = true; - let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); -}; - -/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ -m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) -{ - let civ = gameState.getPlayerCiv(); - let trainableShips = []; - gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { - let trainables = ent.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) - trainableShips.push(trainable); - } - }); - - let best = 0; - let bestShip; - let limits = gameState.getEntityLimits(); - let current = gameState.getEntityCounts(); - for (let trainable of trainableShips) - { - let template = gameState.getTemplate(trainable); - if (!template.available(gameState)) - continue; - - let category = template.trainingCategory(); - if (category && limits[category] && current[category] >= limits[category]) - continue; - - let arrows = +(template.getDefaultArrow() || 0); - if (goal === "attack") // choose the maximum default arrows - { - if (best > arrows) - continue; - best = arrows; - } - else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport - { - let capacity = +(template.garrisonMax() || 0); - if (capacity < 2) - continue; - capacity += 10*arrows; - if (MatchesClassList(template.garrisonableClasses(), "Siege")) - capacity += 50; - if (best > capacity) - continue; - best = capacity; - } - else if (goal === "fishing") - if (!template.hasClass("FishingBoat")) - continue; - bestShip = trainable; - } - return bestShip; -}; - -m.NavalManager.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Naval Manager update"); - - // close previous transport plans if finished - for (let i = 0; i < this.transportPlans.length; ++i) - { - let remaining = this.transportPlans[i].update(gameState); - if (remaining) - continue; - if (this.Config.debug > 1) - API3.warn("no more units on transport plan " + this.transportPlans[i].ID); - this.transportPlans[i].releaseAll(); - this.transportPlans.splice(i--, 1); - } - // assign free ships to plans which need them - this.assignShipsToPlans(gameState); - - // and require for more ships/structures if needed - if (gameState.ai.playedTurn % 3 === 0) - { - this.checkLevels(gameState, queues); - this.maintainFleet(gameState, queues); - this.buildNavalStructures(gameState, queues); - } - // let inactive ships move apart from active ones (waiting for a better pathfinder) - this.moveApart(gameState); - - Engine.ProfileStop(); -}; - -m.NavalManager.prototype.Serialize = function() -{ - let properties = { - "wantedTransportShips": this.wantedTransportShips, - "wantedWarShips": this.wantedWarShips, - "wantedFishShips": this.wantedFishShips, - "neededTransportShips": this.neededTransportShips, - "neededWarShips": this.neededWarShips, - "landingZones": this.landingZones - }; - - let transports = {}; - for (let plan in this.transportPlans) - transports[plan] = this.transportPlans[plan].Serialize(); - - return { "properties": properties, "transports": transports }; -}; - -m.NavalManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.transportPlans = []; - for (let i in data.transports) - { - let dataPlan = data.transports[i]; - let plan = new m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); - plan.Deserialize(dataPlan); - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queue.js b/install/petraBased/petra-single-base/queue.js deleted file mode 100644 index b11b2e1..0000000 --- a/install/petraBased/petra-single-base/queue.js +++ /dev/null @@ -1,167 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Holds a list of wanted plans to train or construct - */ - -m.Queue = function() -{ - this.plans = []; - this.paused = false; - this.switched = 0; -}; - -m.Queue.prototype.empty = function() -{ - this.plans = []; -}; - -m.Queue.prototype.addPlan = function(newPlan) -{ - if (!newPlan) - return; - for (let 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); -}; - -m.Queue.prototype.check= function(gameState) -{ - while (this.plans.length > 0) - { - if (!this.plans[0].isInvalid(gameState)) - return; - let plan = this.plans.shift(); - if (plan.queueToReset) - gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); - } -}; - -m.Queue.prototype.getNext = function() -{ - if (this.plans.length > 0) - return this.plans[0]; - return null; -}; - -m.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 - */ -m.Queue.prototype.maxAccountWanted = function(gameState, fraction) -{ - let cost = new API3.Resources(); - 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) - { - let costs = this.plans[1].getCost(); - costs.multiply(fraction); - cost.add(costs); - } - return cost; -}; - -m.Queue.prototype.queueCost = function() -{ - let cost = new API3.Resources(); - for (let plan of this.plans) - cost.add(plan.getCost()); - return cost; -}; - -m.Queue.prototype.length = function() -{ - return this.plans.length; -}; - -m.Queue.prototype.hasQueuedUnits = function() -{ - return this.plans.length > 0; -}; - -m.Queue.prototype.countQueuedUnits = function() -{ - let count = 0; - for (let plan of this.plans) - count += plan.number; - return count; -}; - -m.Queue.prototype.hasQueuedUnitsWithClass = function(classe) -{ - return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); -}; - -m.Queue.prototype.countQueuedUnitsWithClass = function(classe) -{ - let count = 0; - for (let plan of this.plans) - if (plan.template && plan.template.hasClass(classe)) - count += plan.number; - return count; -}; - -m.Queue.prototype.countQueuedUnitsWithMetadata = function(data, value) -{ - let count = 0; - for (let plan of this.plans) - if (plan.metadata[data] && plan.metadata[data] == value) - count += plan.number; - return count; -}; - -m.Queue.prototype.Serialize = function() -{ - let plans = []; - for (let plan of this.plans) - plans.push(plan.Serialize()); - - return { "plans": plans, "paused": this.paused, "switched": this.switched }; -}; - -m.Queue.prototype.Deserialize = function(gameState, data) -{ - this.paused = data.paused; - this.switched = data.switched; - this.plans = []; - for (let dataPlan of data.plans) - { - let plan; - if (dataPlan.category == "unit") - plan = new m.TrainingPlan(gameState, dataPlan.type); - else if (dataPlan.category == "building") - plan = new m.ConstructionPlan(gameState, dataPlan.type); - else if (dataPlan.category == "technology") - plan = new m.ResearchPlan(gameState, dataPlan.type); - else - { - API3.warn("Petra deserialization error: plan unknown " + uneval(dataPlan)); - continue; - } - plan.Deserialize(gameState, dataPlan); - this.plans.push(plan); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queueManager.js b/install/petraBased/petra-single-base/queueManager.js deleted file mode 100644 index fc0c9dc..0000000 --- a/install/petraBased/petra-single-base/queueManager.js +++ /dev/null @@ -1,599 +0,0 @@ -var PETRA = function(m) -{ - -// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. -// -// Currently this manager keeps accounts for each queue, split between the 4 main resources -// -// Each time resources are available (ie not in any account), it is split between the different queues -// Mostly based on priority of the queue, and existing needs. -// Each turn, the queue Manager checks if a queue can afford its next item, then it does. -// -// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it -// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will -// be able to benefit form the 500 food (even if they only needed food). -// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. -// -// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues -// get some part of the total, and if all queues have 70% of their needs, nothing gets done -// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. -// -// This system should be improved. It's probably not flexible enough. - -m.QueueManager = function(Config, queues) -{ - this.Config = Config; - this.queues = queues; - this.priorities = {}; - for (let i in Config.priorities) - this.priorities[i] = Config.priorities[i]; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in this.queues) - { - this.accounts[q] = new API3.Resources(); - this.queueArrays.push([q, this.queues[q]]); - } - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getAvailableResources = function(gameState) -{ - let resources = gameState.getResources(); - for (let key in this.queues) - resources.subtract(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.getTotalAccountedResources = function() -{ - let resources = new API3.Resources(); - for (let key in this.queues) - resources.add(this.accounts[key]); - return resources; -}; - -m.QueueManager.prototype.currentNeeds = function(gameState) -{ - let needed = new API3.Resources(); - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) - continue; - let costs = queue.plans[0].getCost(); - needed.add(costs); - } - // get out current resources, not removing accounts. - let current = gameState.getResources(); - for (let res of Resources.GetCodes()) - needed[res] = Math.max(0, needed[res] - current[res]); - - return needed; -}; - -// calculate the gather rates we'd want to be able to start all elements in our queues -// TODO: many things. -m.QueueManager.prototype.wantedGatherRates = function(gameState) -{ - // default values for first turn when we have not yet set our queues. - if (gameState.ai.playedTurn === 0) - { - let ret = {}; - for (let res of Resources.GetCodes()) - ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; - return ret; - } - - // get out current resources, not removing accounts. - let current = gameState.getResources(); - // short queue is the first item of a queue, assumed to be ready in 30s - // medium queue is the second item of a queue, assumed to be ready in 60s - // long queue contains the isGo=false items, assumed to be ready in 300s - let totalShort = {}; - let totalMedium = {}; - let totalLong = {}; - for (let res of Resources.GetCodes()) - { - totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; - totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; - totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; - } - let total; - // queueArrays because it's faster. - for (let q of this.queueArrays) - { - let queue = q[1]; - if (queue.paused) - continue; - for (let j = 0; j < queue.length(); ++j) - { - if (j > 1) - break; - let cost = queue.plans[j].getCost(); - if (queue.plans[j].isGo(gameState)) - { - if (j === 0) - total = totalShort; - else - total = totalMedium; - } - else - total = totalLong; - for (let type in total) - total[type] += cost[type]; - if (!queue.plans[j].isGo(gameState)) - break; - } - } - // global rates - let rates = {}; - let diff; - for (let res of Resources.GetCodes()) - { - if (current[res] > 0) - { - diff = Math.min(current[res], totalShort[res]); - totalShort[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - { - diff = Math.min(current[res], totalMedium[res]); - totalMedium[res] -= diff; - current[res] -= diff; - if (current[res] > 0) - totalLong[res] -= Math.min(current[res], totalLong[res]); - } - } - rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; - } - - return rates; -}; - -m.QueueManager.prototype.printQueues = function(gameState) -{ - let numWorkers = 0; - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - numWorkers++; - }); - API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); - for (let i in this.queues) - { - let q = this.queues[i]; - if (q.hasQueuedUnits()) - { - API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); - API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); - } - for (let plan of q.plans) - { - let qStr = " " + plan.type + " "; - if (plan.number) - qStr += "x" + plan.number; - qStr += " isGo " + plan.isGo(gameState); - API3.warn(qStr); - } - } - API3.warn("Accounts"); - for (let p in this.accounts) - API3.warn(p + ": " + uneval(this.accounts[p])); - API3.warn("Current Resources: " + uneval(gameState.getResources())); - API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); - API3.warn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); - API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); - API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); - API3.warn("------------------------------------"); -}; - -m.QueueManager.prototype.clear = function() -{ - for (let i in this.queues) - this.queues[i].empty(); -}; - -/** - * set accounts of queue i from the unaccounted resources - */ -m.QueueManager.prototype.setAccounts = function(gameState, cost, i) -{ - let available = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (this.accounts[i][res] >= cost[res]) - continue; - this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); - } -}; - -/** - * transfer accounts from queue i to queue j - */ -m.QueueManager.prototype.transferAccounts = function(cost, i, j) -{ - for (let res of Resources.GetCodes()) - { - if (this.accounts[j][res] >= cost[res]) - continue; - let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); - this.accounts[i][res] -= diff; - this.accounts[j][res] += diff; - } -}; - -/** - * distribute the resources between the different queues according to their priorities - */ -m.QueueManager.prototype.distributeResources = function(gameState) -{ - let availableRes = this.getAvailableResources(gameState); - for (let res of Resources.GetCodes()) - { - if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) - { - let total = gameState.getResources()[res]; - let scale = total / (total - availableRes[res]); - availableRes[res] = total; - for (let j in this.queues) - { - this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); - availableRes[res] -= this.accounts[j][res]; - } - } - - if (!availableRes[res]) - { - this.switchResource(gameState, res); - continue; - } - - let totalPriority = 0; - let tempPrio = {}; - let maxNeed = {}; - // Okay so this is where it gets complicated. - // If a queue requires "res" for the next elements (in the queue) - // And the account is not high enough for it. - // Then we add it to the total priority. - // To try and be clever, we don't want a long queue to hog all resources. So two things: - // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). - // -queues accounts are capped at "resources for the first + 60% of the next" - // This avoids getting a high priority queue with many elements hogging all of one resource - // uselessly while it awaits for other resources. - for (let j in this.queues) - { - // returns exactly the correct amount, ie 0 if we're not go. - let queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); - if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) - { - // adding us to the list of queues that need an update. - tempPrio[j] = this.priorities[j]; - maxNeed[j] = queueCost[res] - this.accounts[j][res]; - // if we have enough of that resource for our first item in the queue, diminish our priority. - if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) - tempPrio[j] /= 2; - - if (tempPrio[j]) - totalPriority += tempPrio[j]; - } - else if (this.accounts[j][res] > queueCost[res]) - { - availableRes[res] += this.accounts[j][res] - queueCost[res]; - this.accounts[j][res] = queueCost[res]; - } - } - // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" - // But we'll sometimes allow less if that would overflow. - let available = availableRes[res]; - let missing = false; - for (let j in tempPrio) - { - // we'll add at much what can be allowed to this queue. - let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); - if (toAdd >= maxNeed[j]) - toAdd = maxNeed[j]; - else - missing = true; - this.accounts[j][res] += toAdd; - maxNeed[j] -= toAdd; - available -= toAdd; - } - if (missing && available > 0) // distribute the rest (due to floor) in any queue - { - for (let j in tempPrio) - { - let toAdd = Math.min(maxNeed[j], available); - this.accounts[j][res] += toAdd; - available -= toAdd; - if (available <= 0) - break; - } - } - if (available < 0) - API3.warn("Petra: problem with remaining " + res + " in queueManager " + available); - } -}; - -m.QueueManager.prototype.switchResource = function(gameState, res) -{ - // We have no available resources, see if we can't "compact" them in one queue. - // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. - // TODO: this isn't perfect compression. - for (let j in this.queues) - { - if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) - continue; - - let queue = this.queues[j]; - let queueCost = queue.maxAccountWanted(gameState, 0); - if (this.accounts[j][res] >= queueCost[res]) - continue; - - for (let i in this.queues) - { - if (i === j) - continue; - let otherQueue = this.queues[i]; - if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) - continue; - if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) - continue; - - let diff = queueCost[res] - this.accounts[j][res]; - this.accounts[j][res] += diff; - this.accounts[i][res] -= diff; - ++otherQueue.switched; - if (this.Config.debug > 2) - API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); - break; - } - } -}; - -// Start the next item in the queue if we can afford it. -m.QueueManager.prototype.startNextItems = function(gameState) -{ - for (let q of this.queueArrays) - { - let name = q[0]; - let queue = q[1]; - if (queue.hasQueuedUnits() && !queue.paused) - { - let item = queue.getNext(); - if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) - { - // canStart may update the cost because of the costMultiplier so we must check it again - if (this.accounts[name].canAfford(item.getCost())) - { - this.finishingTime = gameState.ai.elapsedTime; - this.accounts[name].subtract(item.getCost()); - queue.startNext(gameState); - queue.switched = 0; - } - } - } - else if (!queue.hasQueuedUnits()) - { - this.accounts[name].reset(); - queue.switched = 0; - } - } -}; - -m.QueueManager.prototype.update = function(gameState) -{ - Engine.ProfileStart("Queue Manager"); - - for (let i in this.queues) - { - this.queues[i].check(gameState); // do basic sanity checks on the queue - if (this.priorities[i] > 0) - continue; - API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); - this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. - } - - // Pause or unpause queues depending on the situation - this.checkPausedQueues(gameState); - - // Let's assign resources to plans that need them - this.distributeResources(gameState); - - // Start the next item in the queue if we can afford it. - this.startNextItems(gameState); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - this.printQueues(gameState); - - Engine.ProfileStop(); -}; - -// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training -m.QueueManager.prototype.checkPausedQueues = function(gameState) -{ - let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); - let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); - for (let q in this.queues) - { - let toBePaused = false; - if (gameState.ai.HQ.numPotentialBases() == 0) - toBePaused = q != "dock" && q != "civilCentre"; - else if (numWorkers < workersMin / 3) - toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; - else if (numWorkers < workersMin * 2 / 3) - toBePaused = q == "civilCentre" || q == "economicBuilding" || - q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || - q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; - else if (numWorkers < workersMin) - toBePaused = q == "civilCentre" || q == "defenseBuilding" || - q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; - - if (toBePaused) - { - if (q == "field" && gameState.ai.HQ.needFarm && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "corral" && gameState.ai.HQ.needCorral && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) - toBePaused = false; - if (q == "dock" && gameState.ai.HQ.needFish && - !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) - toBePaused = false; - if (q == "ships" && gameState.ai.HQ.needFish && - !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) - toBePaused = false; - } - - let queue = this.queues[q]; - if (!queue.paused && toBePaused) - { - queue.paused = true; - this.accounts[q].reset(); - } - else if (queue.paused && !toBePaused) - queue.paused = false; - - // And reduce the batch sizes of attack queues - if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) - { - queue.plans[0].number = 1; - if (queue.plans[1]) - queue.plans[1].number = 1; - } - } -}; - -m.QueueManager.prototype.canAfford = function(queue, cost) -{ - if (!this.accounts[queue]) - return false; - return this.accounts[queue].canAfford(cost); -}; - -m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) -{ - if (!this.queues[queue]) - return; - this.queues[queue].paused = true; - if (scrapAccounts) - this.accounts[queue].reset(); -}; - -m.QueueManager.prototype.unpauseQueue = function(queue) -{ - if (this.queues[queue]) - this.queues[queue].paused = false; -}; - -m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) -{ - for (let q in this.queues) - { - if (q == but) - continue; - if (scrapAccounts) - this.accounts[q].reset(); - this.queues[q].paused = true; - } -}; - -m.QueueManager.prototype.unpauseAll = function(but) -{ - for (let q in this.queues) - if (q != but) - this.queues[q].paused = false; -}; - - -m.QueueManager.prototype.addQueue = function(queueName, priority) -{ - if (this.queues[queueName] !== undefined) - return; - - this.queues[queueName] = new m.Queue(); - this.priorities[queueName] = priority; - this.accounts[queueName] = new API3.Resources(); - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.removeQueue = function(queueName) -{ - if (this.queues[queueName] === undefined) - return; - - delete this.queues[queueName]; - delete this.priorities[queueName]; - delete this.accounts[queueName]; - - this.queueArrays = []; - for (let q in this.queues) - this.queueArrays.push([q, this.queues[q]]); - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.getPriority = function(queueName) -{ - return this.priorities[queueName]; -}; - -m.QueueManager.prototype.changePriority = function(queueName, newPriority) -{ - if (this.Config.debug > 1) - API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); - if (this.queues[queueName] !== undefined) - this.priorities[queueName] = newPriority; - let priorities = this.priorities; - this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); -}; - -m.QueueManager.prototype.Serialize = function() -{ - let accounts = {}; - let queues = {}; - for (let q in this.queues) - { - queues[q] = this.queues[q].Serialize(); - accounts[q] = this.accounts[q].Serialize(); - if (this.Config.debug == -100) - API3.warn("queueManager serialization: queue " + q + " >>> " + - uneval(queues[q]) + " with accounts " + uneval(accounts[q])); - } - - return { - "priorities": this.priorities, - "queues": queues, - "accounts": accounts - }; -}; - -m.QueueManager.prototype.Deserialize = function(gameState, data) -{ - this.priorities = data.priorities; - this.queues = {}; - this.accounts = {}; - - // the sorting is updated on priority change. - this.queueArrays = []; - for (let q in data.queues) - { - this.queues[q] = new m.Queue(); - this.queues[q].Deserialize(gameState, data.queues[q]); - this.accounts[q] = new API3.Resources(); - this.accounts[q].Deserialize(data.accounts[q]); - this.queueArrays.push([q, this.queues[q]]); - } - this.queueArrays.sort((a, b) => data.priorities[b[0]] - data.priorities[a[0]]); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queueplan.js b/install/petraBased/petra-single-base/queueplan.js deleted file mode 100644 index ba4ebf5..0000000 --- a/install/petraBased/petra-single-base/queueplan.js +++ /dev/null @@ -1,70 +0,0 @@ -var PETRA = function(m) -{ -/** - * Common functions and variables to all queue plans. - */ - -m.QueuePlan = function(gameState, type, metadata) -{ - this.type = gameState.applyCiv(type); - this.metadata = metadata; - - this.template = gameState.getTemplate(this.type); - if (!this.template) - { - API3.warn("Tried to add the inexisting template " + this.type + " to Petra."); - return false; - } - this.ID = gameState.ai.uniqueIDs.plans++; - this.cost = new API3.Resources(this.template.cost()); - this.number = 1; - this.category = ""; - - return true; -}; - -/** Check the content of this queue */ -m.QueuePlan.prototype.isInvalid = function(gameState) -{ - return false; -}; - -/** if true, the queue manager will begin increasing this plan's account. */ -m.QueuePlan.prototype.isGo = function(gameState) -{ - return true; -}; - -/** can we start this plan immediately? */ -m.QueuePlan.prototype.canStart = function(gameState) -{ - return false; -}; - -/** process the plan. */ -m.QueuePlan.prototype.start = function(gameState) -{ - // should call onStart. -}; - -m.QueuePlan.prototype.getCost = function() -{ - let costs = new API3.Resources(); - costs.add(this.cost); - if (this.number !== 1) - costs.multiply(this.number); - return costs; -}; - -/** - * On Event functions. - * Can be used to do some specific stuffs - * Need to be updated to actually do something if you want them to. - * this is called by "Start" if it succeeds. - */ -m.QueuePlan.prototype.onStart = function(gameState) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queueplanBuilding.js b/install/petraBased/petra-single-base/queueplanBuilding.js deleted file mode 100644 index 841d5d2..0000000 --- a/install/petraBased/petra-single-base/queueplanBuilding.js +++ /dev/null @@ -1,951 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Defines a construction plan, ie a building. - * We'll try to fing a good position if non has been provided - */ - -m.ConstructionPlan = function(gameState, type, metadata, position) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - return false; - - this.position = position ? position : 0; - - this.category = "building"; - - return true; -}; - -m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ConstructionPlan.prototype.canStart = function(gameState) -{ - if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn - return false; - - if (!this.isGo(gameState)) - return false; - - if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) - return false; - - return gameState.ai.HQ.buildManager.hasBuilder(this.type); -}; - -m.ConstructionPlan.prototype.start = function(gameState) -{ - Engine.ProfileStart("Building construction start"); - - // We don't care which builder we assign, since they won't actually do - // the building themselves - all we care about is that there is at least - // one unit that can start the foundation (should always be the case here). - let builder = gameState.findBuilder(this.type); - if (!builder) - { - API3.warn("petra error: builder not found when starting construction."); - Engine.ProfileStop(); - return; - } - - let pos = this.findGoodPosition(gameState); - if (!pos) - { - gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); - Engine.ProfileStop(); - return; - } - - if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("BarterMarket") || - gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())) - { - // Check if this market is still worth building (others may have been built making it useless) - let tradeManager = gameState.ai.HQ.tradeManager; - tradeManager.checkRoutes(gameState); - if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) - { - Engine.ProfileStop(); - return; - } - } - gameState.ai.HQ.turnCache.buildingBuilt = true; - - if (this.metadata === undefined) - this.metadata = { "base": pos.base }; - else if (this.metadata.base === undefined) - this.metadata.base = pos.base; - - if (pos.access) - this.metadata.access = pos.access; // needed for Docks whose position is on water - else - this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); - - if (this.template.buildPlacementType() == "shore") - { - // adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) - builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); - else // try with the lowest, move towards us unless we're same - { - for (let step = 0; step <= 1; step += 0.2) - builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, - pos.angle, this.metadata); - } - this.onStart(gameState); - Engine.ProfileStop(); - - if (this.metadata && this.metadata.proximity) - gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); -}; - -m.ConstructionPlan.prototype.findGoodPosition = function(gameState) -{ - let template = this.template; - - if (template.buildPlacementType() == "shore") - return this.findDockPosition(gameState); - - let HQ = gameState.ai.HQ; - if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) - { - // recompute the best dropsite location in case some conditions have changed - let base = HQ.getBaseByID(this.metadata.base); - let type = this.metadata.type ? this.metadata.type : "wood"; - let newpos = base.findBestDropsiteLocation(gameState, type); - if (newpos && newpos.quality > 0) - { - let pos = newpos.pos; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; - } - } - - if (!this.position) - { - if (template.hasClass("CivCentre")) - { - let pos; - if (this.metadata && this.metadata.resource) - { - let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; - pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); - } - else - pos = HQ.findStrategicCCLocation(gameState, template); - - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; - // No possible location, try to build instead a dock in a not-enemy island - let templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) - { - template = gameState.getTemplate(templateName); - if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) - this.buildOverseaDock(gameState, template); - } - return false; - } - else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) - { - let pos = HQ.findDefensiveLocation(gameState, template); - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - // if this fortress is our first one, just try the standard placement - if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) - return false; - } - else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before - { - let pos = HQ.findMarketLocation(gameState, template); - if (pos && pos[2] > 0) - { - if (!this.metadata) - this.metadata = {}; - this.metadata.expectedGain = pos[3]; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - } - else if (!pos) - return false; - } - } - - // Compute each tile's closeness to friendly structures: - - let placement = new API3.Map(gameState.sharedScript, "territory"); - let cellSize = placement.cellSize; // size of each tile - - let alreadyHasHouses = false; - - if (this.position) // If a position was specified then place the building as close to it as possible - { - let x = Math.floor(this.position[0] / cellSize); - let z = Math.floor(this.position[1] / cellSize); - placement.addInfluence(x, z, 255); - } - else // No position was specified so try and find a sensible place to build - { - // give a small > 0 level as the result of addInfluence is constrained to be > 0 - // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] == base) - placement.set(j, 45); - } - else - { - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] != 0) - placement.set(j, 45); - } - - if (!HQ.requireHouses || !template.hasClass("House")) - { - gameState.getOwnStructures().forEach(function(ent) { - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - - let struct = m.getBuiltEntity(gameState, ent); - if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) - { - if (template.hasClass("Field") || template.hasClass("Corral")) - placement.addInfluence(x, z, 80/cellSize, 50); - else // If this is not a field add a negative influence because we want to leave this area for fields - placement.addInfluence(x, z, 80/cellSize, -20); - } - else if (template.hasClass("House")) - { - if (ent.hasClass("House")) - { - placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses - alreadyHasHouses = true; - } - else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) - placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs - } - else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && - (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) - placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) - else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) - placement.addInfluence(x, z, 120/cellSize, -50); - else if (template.hasClass("Military")) - placement.addInfluence(x, z, 40/cellSize, -40); - else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) - placement.addInfluence(x, z, 60/cellSize, 40); - }); - } - if (template.hasClass("Farmstead")) - { - for (let j = 0; j < placement.map.length; ++j) - { - let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; - if (HQ.borderMap.map[j] & m.fullBorder_Mask) - value /= 2; // we need space around farmstead, so disfavor map border - placement.set(j, value); - } - } - } - - // Requires to be inside our territory, and inside our base territory if required - // and if our first market, put it on border if possible to maximize distance with next market - let favorBorder = template.hasClass("BarterMarket"); - let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); - let favoredBase = this.metadata && (this.metadata.favoredBase || - (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] != base) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - } - } - } - else - { - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] == 0) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - else if (favoredBase && HQ.basesMap.map[j] == favoredBase) - placement.set(j, placement.map[j] + 100); - } - } - } - - // Find the best non-obstructed: - // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, - // this allows room for units to walk between buildings. - // note: not for houses and dropsites who ought to be closer to either each other or a resource. - // also not for fields who can be stacked quite a bit - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let radius = 0; - if (template.hasClass("Fortress") || template.hasClass("Workshop") || - this.type == gameState.applyCiv("structures/{civ}_elephant_stables")) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && - !template.hasClass("Field") && !template.hasClass("BarterMarket")) - radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let bestTile; - if (template.hasClass("House") && !alreadyHasHouses) - { - // try to get some space to place several houses first - bestTile = placement.findBestTile(3*radius, obstructions); - if (!bestTile.val) - bestTile = undefined; - } - - if (!bestTile) - bestTile = placement.findBestTile(radius, obstructions); - - if (!bestTile.val) - return false; - - let bestIdx = bestTile.idx; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - let territorypos = placement.gamePosToMapPos([x, z]); - let territoryIndex = territorypos[0] + territorypos[1]*placement.width; - // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; -}; - -/** - * Placement of buildings with Dock build category - * metadata.proximity is defined when first dock without any territory - * => we try to minimize distance from our current point - * metadata.oversea is defined for dock in oversea islands - * => we try to maximize distance to our current docks (for trade) - * otherwise standard dock on an island where we already have a cc - * => we try not to be too far from our territory - * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. - */ -m.ConstructionPlan.prototype.findDockPosition = function(gameState) -{ - let template = this.template; - let territoryMap = gameState.ai.HQ.territoryMap; - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let bestIdx; - let bestJdx; - let bestAngle; - let bestLand; - let bestWater; - let bestVal = -1; - let navalPassMap = gameState.ai.accessibility.navalPassMap; - - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - - let nbShips = gameState.ai.HQ.navalManager.transportShips.length; - let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; - let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; - let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; - let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; - if (nbShips == 0 && proxyAccess && proxyAccess > 1) - { - wantedLand = {}; - wantedLand[proxyAccess] = true; - } - let dropsiteTypes = template.resourceDropsiteTypes(); - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let halfSize = 0; // used for dock angle - let halfDepth = 0; // used by checkPlacement - let halfWidth = 0; // used by checkPlacement - if (template.get("Footprint/Square")) - { - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - halfDepth = +template.get("Footprint/Square/@depth") / 2; - halfWidth = +template.get("Footprint/Square/@width") / 2; - } - else if (template.get("Footprint/Circle")) - { - halfSize = +template.get("Footprint/Circle/@radius"); - halfDepth = halfSize; - halfWidth = halfSize; - } - - // res is a measure of the amount of resources around, and maxRes is the max value taken into account - // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement - const maxRes = 10; - const maxWater = 16; - let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; - let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; - // Normalisation factors (only guessed, no attempt to optimize them) - let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; - for (let j = 0; j < territoryMap.length; ++j) - { - if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) - continue; - let score = 0; - if (!proxyAccess && !oversea) - { - // if not in our (or allied) territory, we do not want it too far to be able to defend it - score = this.getFrontierProximity(gameState, j); - if (score > 4) - continue; - score *= factor; - } - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - if (wantedSea && navalPassMap[i] != wantedSea) - continue; - - let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - // If proximity is given, we look for the nearest point - if (proxyAccess) - score = API3.VectorDistance(this.metadata.proximity, pos); - - // Bonus for resources - score += 20 * (maxRes - res); - - if (oversea) - { - // Not much farther to one of our cc than to enemy ones - let enemyDist; - let ownDist; - for (let cc of ccEnts.values()) - { - let owner = cc.owner(); - if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) - continue; - let dist = API3.SquareVectorDistance(pos, cc.position()); - if (owner == PlayerID && (!ownDist || dist < ownDist)) - ownDist = dist; - if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) - enemyDist = dist; - } - if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) - continue; - - // And maximize distance for trade. - let dockDist = 0; - for (let dock of docks.values()) - { - if (m.getSeaAccess(gameState, dock) != navalPassMap[i]) - continue; - let dist = API3.SquareVectorDistance(pos, dock.position()); - if (dist > dockDist) - dockDist = dist; - } - if (dockDist > 0) - { - dockDist = Math.sqrt(dockDist); - if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away - continue; - score += factor * (width * cellSize - dockDist); - } - } - - // Add a penalty if on the map border as ship movement will be difficult - if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) - score += 20; - - // Do a pre-selection, supposing we will have the best possible water - if (bestIdx !== undefined && score > bestVal + 5 * maxWater) - continue; - - let x = (i % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; - let angle = this.getDockAngle(gameState, x, z, halfSize); - if (angle == false) - continue; - let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); - if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) - continue; - // Final selection now that the checkDockPlacement water is known - if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) - continue; - if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - - bestVal = score + maxWater - ret.water; - bestIdx = i; - bestJdx = j; - bestAngle = angle; - bestLand = ret.land; - bestWater = ret.water; - } - if (bestVal < 0) - return false; - - // if no good place with enough water around and still in first phase, wait for expansion at the next phase - if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Assign this dock to a base - let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; - if (!baseIndex) - baseIndex = -2; // We'll do an anchorless base around it - - return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; -}; - -/** - * Find a good island to build a dock. - */ -m.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) -{ - let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); - if (!docks.hasEntities()) - return; - - let passabilityMap = gameState.getPassabilityMap(); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - - let land = {}; - let found; - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (gameState.ai.accessibility.regionType[i] != "land" || - cellArea * gameState.ai.accessibility.regionSize[i] < 3600) - continue; - let keep = true; - for (let dock of docks.values()) - { - if (m.getLandAccess(gameState, dock) != i) - continue; - keep = false; - break; - } - if (!keep) - continue; - let sea; - for (let cc of ccEnts.values()) - { - let ccAccess = m.getLandAccess(gameState, cc); - if (ccAccess != i) - { - if (cc.owner() == PlayerID && !sea) - sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); - continue; - } - // Docks on island where we have a cc are already done elsewhere - if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) - { - keep = false; - break; - } - } - if (!keep || !sea) - continue; - land[i] = true; - found = true; - } - if (!found) - return; - if (!gameState.ai.HQ.navalMap) - API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); - - let oldTemplate = this.template; - let oldMetadata = this.metadata; - this.template = template; - let pos; - this.metadata = { "land": land, "oversea": true }; - pos = this.findDockPosition(gameState); - if (pos) - { - let type = template.templateName(); - let builder = gameState.findBuilder(type); - this.metadata.base = pos.base; - // Adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - this.template = oldTemplate; - this.metadata = oldMetadata; -}; - -/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ -m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) -{ - let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*gameState.ai.accessibility.width; - let seaRef = gameState.ai.accessibility.navalPassMap[k]; - if (seaRef < 2) - return false; - const numPoints = 16; - for (let dist = 0; dist < 4; ++dist) - { - let waterPoints = []; - for (let i = 0; i < numPoints; ++i) - { - let angle = 2 * Math.PI * i / numPoints; - pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; - pos = gameState.ai.accessibility.gamePosToMapPos(pos); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - continue; - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.navalPassMap[j] == seaRef) - waterPoints.push(i); - } - let length = waterPoints.length; - if (!length) - continue; - let consec = []; - for (let i = 0; i < length; ++i) - { - let count = 0; - for (let j = 0; j < length-1; ++j) - { - if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) - ++count; - else - break; - } - consec[i] = count; - } - let start = 0; - let count = 0; - for (let c in consec) - { - if (consec[c] > count) - { - start = c; - count = consec[c]; - } - } - - // If we've found a shoreline, stop searching - if (count != numPoints-1) - return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; - } - return false; -}; - -/** - * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js - * to determine the special dock requirements - * returns {"land": land index for this dock, "water": amount of water around this spot} - */ -m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) -{ - let sz = halfDepth * Math.sin(angle); - let cz = halfDepth * Math.cos(angle); - // center back position - let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[j]; - if (land < 2) - return null; - // center front position - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - return null; - // additional constraints compared to BuildRestriction.js to assure we have enough place to build - let sw = halfWidth * Math.cos(angle) * 3 / 4; - let cw = halfWidth * Math.sin(angle) * 3 / 4; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - let water = 0; - let sp = 15 * Math.sin(angle); - let cp = 15 * Math.cos(angle); - for (let i = 1; i < 5; ++i) - { - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - water += 4; - } - return { "land": land, "water": water }; -}; - -/** - * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension - * if the (object) wantedLand is given, this nearest land should have one of these accessibility - * if wantedSea is given, this tile should be inside this sea - */ -const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], - [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50]]; - -m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) -{ - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - let dimLand = dimension + 1.5 * cellSize; - let dimSea = dimension + 2 * cellSize; - - let accessibility = gameState.ai.accessibility; - let x = (j%width + 0.5) * cellSize; - let z = (Math.floor(j/width) + 0.5) * cellSize; - let pos = accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*accessibility.width; - let landPass = accessibility.landPassMap[k]; - if (landPass > 1 && wantedLand && !wantedLand[landPass] || - landPass < 2 && accessibility.navalPassMap[k] < 2) - return false; - - for (let a of around) - { - pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - landPass = accessibility.landPassMap[k]; - if (landPass < 2 || wantedLand && !wantedLand[landPass]) - continue; - pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - if (wantedSea && accessibility.navalPassMap[k] != wantedSea || - !wantedSea && accessibility.navalPassMap[k] < 2) - continue; - return true; - } - - return false; -}; - -/** - * return a measure of the proximity to our frontier (including our allies) - * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m - */ -m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) -{ - let alliedVictory = gameState.getAlliedVictory(); - let territoryMap = gameState.ai.HQ.territoryMap; - let territoryOwner = territoryMap.getOwnerIndex(j); - if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) - return 0; - - let borderMap = gameState.ai.HQ.borderMap; - let width = territoryMap.width; - let step = Math.round(24 / territoryMap.cellSize); - let ix = j % width; - let iz = Math.floor(j / width); - let best = 5; - for (let a of around) - { - for (let i = 1; i < 5; ++i) - { - let jx = ix + Math.round(i*step*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(i*step*a[1]); - if (jz < 0 || jz >= width) - continue; - if (borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); - if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) - { - best = Math.min(best, i); - break; - } - } - if (best == 1) - break; - } - - return best; -}; - -/** - * get the sum of the resources (except food) around, inside a given radius - * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood - */ -m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) -{ - let resourceMaps = gameState.sharedScript.resourceMaps; - let w = resourceMaps.wood.width; - let cellSize = resourceMaps.wood.cellSize; - let size = Math.floor(radius / cellSize); - let ix = i % w; - let iy = Math.floor(i / w); - let total = 0; - let nbcell = 0; - for (let k of types) - { - if (k == "food" || !resourceMaps[k]) - continue; - let weigh0 = k == "wood" ? 2 : 1; - for (let dy = 0; dy <= size; ++dy) - { - let dxmax = size - dy; - let ky = iy + dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - if (dy == 0) - continue; - ky = iy - dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - } - } - return nbcell ? total / nbcell : 0; -}; - -m.ConstructionPlan.prototype.isGo = function(gameState) -{ - if (this.goRequirement && this.goRequirement == "houseNeeded") - { - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) - return false; - if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return false; - let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); - for (let ent of gameState.getOwnFoundations().values()) - { - let template = gameState.getBuiltTemplate(ent.templateName()); - if (template) - freeSlots += template.getPopulationBonus(); - } - - if (gameState.ai.HQ.saveResources) - return freeSlots <= 10; - if (gameState.getPopulation() > 55) - return freeSlots <= 21; - if (gameState.getPopulation() > 30) - return freeSlots <= 15; - return freeSlots <= 10; - } - return true; -}; - -m.ConstructionPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); -}; - -m.ConstructionPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "position": this.position, - "goRequirement": this.goRequirement || undefined, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ConstructionPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queueplanResearch.js b/install/petraBased/petra-single-base/queueplanResearch.js deleted file mode 100644 index 2e09783..0000000 --- a/install/petraBased/petra-single-base/queueplanResearch.js +++ /dev/null @@ -1,113 +0,0 @@ -var PETRA = function(m) -{ - -m.ResearchPlan = function(gameState, type, rush = false) -{ - if (!m.QueuePlan.call(this, gameState, type, {})) - return false; - - if (this.template.researchTime === undefined) - return false; - - // Refine the estimated cost - let researchers = this.getBestResearchers(gameState, true); - if (researchers) - this.cost = new API3.Resources(this.template.cost(researchers[0])); - - this.category = "technology"; - this.rush = rush; - - return true; -}; - -m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ResearchPlan.prototype.canStart = function(gameState) -{ - this.researchers = this.getBestResearchers(gameState); - if (!this.researchers) - return false; - this.cost = new API3.Resources(this.template.cost(this.researchers[0])); - return true; -}; - -m.ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false) -{ - let allResearchers = gameState.findResearchers(this.type, noRequirementCheck); - if (!allResearchers || !allResearchers.hasEntities()) - return undefined; - - // Keep only researchers with smallest cost - let costMin = Math.min(); - let researchers; - for (let ent of allResearchers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - researchers.push(ent); - else if (cost < costMin) - { - costMin = cost; - researchers = [ent]; - } - } - return researchers; -}; - -m.ResearchPlan.prototype.isInvalid = function(gameState) -{ - return gameState.isResearched(this.type) || gameState.isResearching(this.type); -}; - -m.ResearchPlan.prototype.start = function(gameState) -{ - // Prefer researcher with shortest queues (no need to serialize this.researchers - // as the functions canStart and start are always called on the same turn) - this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); - // Drop anything in the queue if we rush it. - if (this.rush) - this.researchers[0].stopAllProduction(0.45); - this.researchers[0].research(this.type); - this.onStart(gameState); -}; - -m.ResearchPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); - - for (let i = gameState.getNumberOfPhases(); i > 0; --i) - { - if (this.type != gameState.getPhaseName(i)) - continue; - gameState.ai.HQ.phasing = 0; - gameState.ai.HQ.OnPhaseUp(gameState, i); - break; - } -}; - -m.ResearchPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "rush": this.rush, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ResearchPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/queueplanTraining.js b/install/petraBased/petra-single-base/queueplanTraining.js deleted file mode 100644 index e63e387..0000000 --- a/install/petraBased/petra-single-base/queueplanTraining.js +++ /dev/null @@ -1,193 +0,0 @@ -var PETRA = function(m) -{ - -m.TrainingPlan = function(gameState, type, metadata, number = 1, maxMerge = 5) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - { - API3.warn(" Plan training " + type + " canceled"); - return false; - } - - // Refine the estimated cost and add pop cost - let trainers = this.getBestTrainers(gameState); - let trainer = trainers ? trainers[0] : undefined; - this.cost = new API3.Resources(this.template.cost(trainer), +this.template._template.Cost.Population); - - this.category = "unit"; - this.number = number; - this.maxMerge = maxMerge; - - return true; -}; - -m.TrainingPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.TrainingPlan.prototype.canStart = function(gameState) -{ - this.trainers = this.getBestTrainers(gameState); - if (!this.trainers) - return false; - this.cost = new API3.Resources(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); - return true; -}; - -m.TrainingPlan.prototype.getBestTrainers = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let trainer = gameState.getEntityById(this.metadata.trainer); - if (trainer) - return [trainer]; - } - - let allTrainers = gameState.findTrainers(this.type); - if (this.metadata && this.metadata.sea) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)); - if (this.metadata && this.metadata.base) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "base", this.metadata.base)); - if (!allTrainers || !allTrainers.hasEntities()) - return undefined; - - // Keep only trainers with smallest cost - let costMin = Math.min(); - let trainers; - for (let ent of allTrainers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - trainers.push(ent); - else if (cost < costMin) - { - costMin = cost; - trainers = [ent]; - } - } - return trainers; -}; - -m.TrainingPlan.prototype.start = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let metadata = {}; - for (let key in this.metadata) - if (key !== "trainer") - metadata[key] = this.metadata[key]; - this.metadata = metadata; - } - - if (this.trainers.length > 1) - { - let wantedIndex; - if (this.metadata && this.metadata.index) - wantedIndex = this.metadata.index; - let workerUnit = this.metadata && this.metadata.role && this.metadata.role == "worker"; - let supportUnit = this.template.hasClass("Support"); - this.trainers.sort(function(a, b) { - // Prefer training buildings with short queues - let aa = a.trainingQueueTime(); - let bb = b.trainingQueueTime(); - // Give priority to support units in the cc - if (a.hasClass("Civic") && !supportUnit) - aa += 10; - if (b.hasClass("Civic") && !supportUnit) - bb += 10; - // And support units should not be too near to dangerous place - if (supportUnit) - { - if (gameState.ai.HQ.isNearInvadingArmy(a.position())) - aa += 50; - if (gameState.ai.HQ.isNearInvadingArmy(b.position())) - bb += 50; - } - // Give also priority to buildings with the right accessibility - let aBase = a.getMetadata(PlayerID, "base"); - let bBase = b.getMetadata(PlayerID, "base"); - if (wantedIndex) - { - if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) - aa += 30; - if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) - bb += 30; - } - // Then, if workers, small preference for bases with less workers - if (workerUnit && aBase && bBase && aBase != bBase) - { - let apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; - let bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; - if (apop > bpop) - aa++; - else if (bpop > apop) - bb++; - } - return aa - bb; - }); - } - - if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) - this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); - this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState)); - - this.onStart(gameState); -}; - -m.TrainingPlan.prototype.addItem = function(amount = 1) -{ - this.number += amount; -}; - -/** Find the promoted types corresponding to this.type */ -m.TrainingPlan.prototype.promotedTypes = function(gameState) -{ - let types = []; - let promotion = this.template.promotion(); - let previous; - let template; - while (promotion) - { - types.push(promotion); - previous = promotion; - template = gameState.getTemplate(promotion); - if (!template) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" promotion template " + promotion + " is not found"); - promotion = undefined; - break; - } - promotion = template.promotion(); - if (previous === promotion) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" unit " + promotion + " is its own promoted unit"); - promotion = undefined; - } - } - return types; -}; - -m.TrainingPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "maxMerge": this.maxMerge - }; -}; - -m.TrainingPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/researchManager.js b/install/petraBased/petra-single-base/researchManager.js deleted file mode 100644 index 51ce3ff..0000000 --- a/install/petraBased/petra-single-base/researchManager.js +++ /dev/null @@ -1,244 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the research - */ - -m.ResearchManager = function(Config) -{ - this.Config = Config; -}; - -/** - * Check if we can go to the next phase - */ -m.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; - - let currentPhaseIndex = gameState.currentPhase(); - let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); - if (!nextPhaseName) - return; - - let 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 m.ResearchPlan(gameState, nextPhaseName, true)); - } -}; - -m.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 !== "Cost/PopulationBonus") - continue; - queues.minorTech.addPlan(new m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -m.ResearchManager.prototype.researchTradeBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -/** Techs to be searched for as soon as they are available */ -m.ResearchManager.prototype.researchWantedTechs = function(gameState, techs) -{ - let phase1 = gameState.currentPhase() === 1; - let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase1) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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 */ -m.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) -{ - let phase2 = gameState.currentPhase() === 2; - let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase2) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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; -}; - -m.ResearchManager.prototype.update = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - - let techName = this.researchWantedTechs(gameState, techs); - if (techName) - { - if (techName.increasePriority) - { - gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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) - { - let template = techs[i][1]._template; - if (template.affects && template.affects.length === 1 && - (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "StoneWall")) - { - techs.splice(i--, 1); - continue; - } - if (template.modifications && template.modifications.length === 1 && - template.modifications[0].value === "Player/sharedLos" && - !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 m.ResearchPlan(gameState, pickRandom(techs)[0])); -}; - -m.ResearchManager.prototype.CostSum = function(cost) -{ - let costSum = 0; - for (let res in cost) - costSum += cost[res]; - return costSum; -}; - -m.ResearchManager.prototype.Serialize = function() -{ - return {}; -}; - -m.ResearchManager.prototype.Deserialize = function(data) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/startingStrategy.js b/install/petraBased/petra-single-base/startingStrategy.js deleted file mode 100644 index db76f30..0000000 --- a/install/petraBased/petra-single-base/startingStrategy.js +++ /dev/null @@ -1,578 +0,0 @@ -var PETRA = function(m) -{ -/** - * determines the strategy to adopt when starting a new game, depending on the initial conditions - */ - -m.HQ.prototype.gameAnalysis = function(gameState) -{ - // Analysis of the terrain and the different access regions - if (!this.regionAnalysis(gameState)) - return; - - this.attackManager.init(gameState); - this.buildManager.init(gameState); - this.navalManager.init(gameState); - this.tradeManager.init(gameState); - this.diplomacyManager.init(gameState); - - // Make a list of buildable structures from the config file - this.structureAnalysis(gameState); - - // Let's get our initial situation here. - let nobase = new m.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); - this.updateTerritories(gameState); - - // Assign entities and resources in the different bases - this.assignStartingEntities(gameState); - - - // Sandbox difficulty should not try to expand - this.canExpand = this.Config.difficulty != 0; - // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks - this.canBuildUnits = true; - if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) - { - let template = gameState.applyCiv("structures/{civ}_civil_centre"); - if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) - { - if (this.Config.debug > 1) - API3.warn(" this AI is unable to produce any units"); - this.canBuildUnits = false; - this.dispatchUnits(gameState); - } - else - this.buildFirstBase(gameState); - } - - // configure our first base strategy - if (this.baseManagers.length > 1) - this.configFirstBase(gameState); -}; - -/** - * Assign the starting entities to the different bases - */ -m.HQ.prototype.assignStartingEntities = function(gameState) -{ - for (let ent of gameState.getOwnEntities().values()) - { - // do not affect merchant ship immediately to trade as they may-be useful for transport - if (ent.hasClass("Trader") && !ent.hasClass("Ship")) - this.tradeManager.assignTrader(ent); - - let pos = ent.position(); - if (!pos) - { - // TODO should support recursive garrisoning. Make a warning for now - if (ent.isGarrisonHolder() && ent.garrisoned().length) - API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); - continue; - } - - // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - if (land > 1 && !this.landRegions[land]) - this.landRegions[land] = true; - let sea = gameState.ai.accessibility.navalPassMap[index]; - if (sea > 1 && !this.navalRegions[sea]) - this.navalRegions[sea] = true; - - // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport - // when a construction will start (see createTransportIfNeeded) - if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) - for (let id of ent.garrisoned()) - ent.unload(id); - - let bestbase; - let territorypos = this.territoryMap.gamePosToMapPos(pos); - let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; - for (let i = 1; i < this.baseManagers.length; ++i) - { - let base = this.baseManagers[i]; - 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, "anchorless"); - else - bestbase = m.getBestBase(gameState, ent) || this.baseManagers[0]; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (pos && bestbase.ID !== this.baseManagers[0].ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === "worker") - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } - } -}; - -/** - * determine the main land Index (or water index if none) - * as well as the list of allowed (land andf water) regions - */ -m.HQ.prototype.regionAnalysis = function(gameState) -{ - let accessibility = gameState.ai.accessibility; - let landIndex; - let seaIndex; - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - let land = accessibility.getAccessValue(cc.position()); - if (land > 1) - { - landIndex = land; - break; - } - } - if (!landIndex) - { - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) - continue; - let land = accessibility.getAccessValue(ent.position()); - if (land > 1) - { - landIndex = land; - break; - } - let sea = accessibility.getAccessValue(ent.position(), true); - if (!seaIndex && sea > 1) - seaIndex = sea; - } - } - if (!landIndex && !seaIndex) - { - API3.warn("Petra error: it does not know how to interpret this map"); - return false; - } - - let passabilityMap = gameState.getPassabilityMap(); - let totalSize = passabilityMap.width * passabilityMap.width; - let minLandSize = Math.floor(0.1*totalSize); - let minWaterSize = Math.floor(0.2*totalSize); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - for (let i = 0; i < accessibility.regionSize.length; ++i) - { - if (landIndex && i == landIndex) - this.landRegions[i] = true; - else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) - { - if (landIndex) - { - let sea = this.getSeaBetweenIndices(gameState, landIndex, i); - if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[sea] = true; - } - } - else - { - let traject = accessibility.getTrajectToIndex(seaIndex, i); - if (traject && traject.length === 2) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[seaIndex] = true; - } - } - } - else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) - { - this.navalMap = true; - this.navalRegions[i] = true; - } - else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) - this.navalRegions[i] = true; - } - - if (this.Config.debug < 3) - return true; - for (let region in this.landRegions) - API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); - API3.warn(" navalMap " + this.navalMap); - API3.warn(" landRegions " + uneval(this.landRegions)); - API3.warn(" navalRegions " + uneval(this.navalRegions)); - return true; -}; - -/** - * load units and buildings from the config files - * TODO: change that to something dynamic - */ -m.HQ.prototype.structureAnalysis = function(gameState) -{ - let civref = gameState.playerData.civ; - let civ = civref in this.Config.buildings ? civref : 'default'; - this.bAdvanced = []; - for (let building of this.Config.buildings[civ]) - if (gameState.isTemplateAvailable(gameState.applyCiv(building))) - this.bAdvanced.push(gameState.applyCiv(building)); -}; - -/** - * build our first base - * if not enough resource, try first to do a dock - */ -m.HQ.prototype.buildFirstBase = function(gameState) -{ - if (gameState.ai.queues.civilCentre.hasQueuedUnits()) - return; - let templateName = gameState.applyCiv("structures/{civ}_civil_centre"); - if (gameState.isTemplateDisabled(templateName)) - return; - let template = gameState.getTemplate(templateName); - if (!template) - return; - let total = gameState.getResources(); - let goal = "civil_centre"; - if (!total.canAfford(new API3.Resources(template.cost()))) - { - let totalExpected = gameState.getResources(); - // Check for treasures around available in some maps at startup - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position()) - continue; - // If we can get a treasure around, just do it - if (ent.isIdle()) - m.gatherTreasure(gameState, ent); - // Then count the resources from the treasures being collected - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (!supplyId) - continue; - let supply = gameState.getEntityById(supplyId); - if (!supply || supply.resourceSupplyType().generic != "treasure") - continue; - let type = supply.resourceSupplyType().specific; - if (!(type in totalExpected)) - continue; - totalExpected[type] += supply.resourceSupplyMax(); - // If we can collect enough resources from these treasures, wait for them - if (totalExpected.canAfford(new API3.Resources(template.cost()))) - return; - } - - // not enough resource to build a cc, try with a dock to accumulate resources if none yet - if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) - { - if (gameState.ai.queues.dock.hasQueuedUnits()) - return; - templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.isTemplateDisabled(templateName)) - return; - template = gameState.getTemplate(templateName); - if (!template || !total.canAfford(new API3.Resources(template.cost()))) - return; - goal = "dock"; - } - } - if (!this.canBuild(gameState, templateName)) - return; - - // We first choose as startingPoint the point where we have the more units - let startingPoint = []; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) - continue; - if (ent.hasClass("Cavalry")) - continue; - let pos = ent.position(); - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - continue; - pos = holder.position(); - } - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - let sea = gameState.ai.accessibility.navalPassMap[index]; - let found = false; - for (let point of startingPoint) - { - if (land !== point.land || sea !== point.sea) - continue; - if (API3.SquareVectorDistance(point.pos, pos) > 2500) - continue; - point.weight += 1; - found = true; - break; - } - if (!found) - startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 }); - } - if (!startingPoint.length) - return; - - let imax = 0; - for (let i = 1; i < startingPoint.length; ++i) - if (startingPoint[i].weight > startingPoint[imax].weight) - imax = i; - - if (goal == "dock") - { - let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; - gameState.ai.queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); - } - else - gameState.ai.queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); -}; - -/** - * set strategy if game without construction: - * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack - * - otherwise all units will attack - */ -m.HQ.prototype.dispatchUnits = function(gameState) -{ - let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - if (allycc.length) - { - if (this.Config.debug > 1) - API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); - let units = gameState.getOwnUnits(); - let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); - let num1 = Math.floor(num / 2); - let num2 = num1; - // first pass to affect ranged infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => { - if (!num || !num1) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num1; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // second pass to affect melee infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => { - if (!num || !num2) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num2; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // and now complete the affectation, including all support units - units.forEach(ent => { - if (!num && !ent.hasClass("Support")) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - if (!ent.hasClass("Support")) - --num; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - } -}; - -/** - * configure our first base expansion - * - if on a small island, favor fishing - * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - */ -m.HQ.prototype.configFirstBase = function(gameState) -{ - if (this.baseManagers.length < 2) - return; - - this.firstBaseConfig = true; - - let startingSize = 0; - let startingLand = []; - for (let region in this.landRegions) - { - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex != +region) - continue; - startingSize += gameState.ai.accessibility.regionSize[region]; - startingLand.push(base.accessIndex); - break; - } - } - let cell = gameState.getPassabilityMap().cellSize; - startingSize = startingSize * cell * cell; - if (this.Config.debug > 1) - API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); - if (startingSize < 25000) - { - this.saveSpace = true; - this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); - let num = Math.max(this.Config.Economy.targetNumFishers, 2); - for (let land of startingLand) - { - for (let sea of gameState.ai.accessibility.regionLinks[land]) - if (gameState.ai.HQ.navalRegions[sea]) - this.navalManager.updateFishingBoats(sea, num); - } - this.maxFields = 1; - this.needCorral = true; - } - else if (startingSize < 60000) - this.maxFields = 2; - else - this.maxFields = false; - - // - count the available wood resource, and react accordingly - let startingFood = gameState.getResources().food; - let check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.food[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingFood += supply.ent.resourceSupplyAmount(); - } - } - } - if (startingFood < 800) - { - if (startingSize < 25000) - { - this.needFish = true; - this.Config.Economy.popForDock = 1; - } - else - this.needFarm = true; - } - // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - let startingWood = gameState.getResources().wood; - check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.wood[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingWood += supply.ent.resourceSupplyAmount(); - } - } - } - if (this.Config.debug > 1) - API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); - if (startingWood < 6000) - { - this.saveResources = true; - this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand - - if (startingWood < 2000 && this.needFarm) - { - this.needCorral = true; - this.needFarm = false; - } - } - if (startingWood > 8500 && this.canBuildUnits) - { - let allowed = Math.ceil((startingWood - 8500) / 3000); - // Not useful to prepare rushing if too long ceasefire - if (gameState.isCeasefireActive()) - { - if (gameState.ceasefireTimeRemaining > 900) - allowed = 0; - else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) - allowed = 1; - } - this.attackManager.setRushes(allowed); - } - - // immediatly build a wood dropsite if possible. - let template = gameState.applyCiv("structures/{civ}_storehouse"); - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) - { - let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); - if (newDP.quality > 40) - { - // if we start with enough workers, put our available resources in this first dropsite - // same thing if our pop exceed the allowed one, as we will need several houses - let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; - if (numWorkers > 12 && newDP.quality > 60 || - gameState.getPopulation() > gameState.getPopulationLimit() + 20) - { - let cost = new API3.Resources(gameState.getTemplate(template).cost()); - gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); - } - gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); - } - } - // and build immediately a corral if needed - if (this.needCorral) - { - template = gameState.applyCiv("structures/{civ}_corral"); - if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) - gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); - } -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-single-base/tradeManager.js b/install/petraBased/petra-single-base/tradeManager.js deleted file mode 100644 index 0878f6c..0000000 --- a/install/petraBased/petra-single-base/tradeManager.js +++ /dev/null @@ -1,723 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the trade - */ - -m.TradeManager = function(Config) -{ - this.Config = Config; - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - this.routeProspection = false; - this.targetNumTraders = this.Config.Economy.targetNumTraders; - this.warnedAllies = {}; -}; - -m.TradeManager.prototype.init = function(gameState) -{ - this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); - this.traders.registerUpdates(); - this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; -}; - -m.TradeManager.prototype.hasTradeRoute = function() -{ - return this.tradeRoute !== undefined; -}; - -m.TradeManager.prototype.assignTrader = function(ent) -{ - ent.setMetadata(PlayerID, "role", "trader"); - this.traders.updateEnt(ent); -}; - -m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) -{ - if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) - return; - - let numTraders = this.traders.length; - let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length; - let numLandTraders = numTraders - numSeaTraders; - // add traders already in training - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "trader") - continue; - numTraders += item.count; - if (item.metadata.sea !== undefined) - numSeaTraders += item.count; - else - numLandTraders += item.count; - } - }); - if (numTraders >= this.targetNumTraders && - (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || - this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) - return; - - let template; - let metadata = { "role": "trader" }; - if (this.tradeRoute.sea) - { - // if we have some merchand ships affected to transport, try first to reaffect them - // May-be, there were produced at an early stage when no other ship were available - // and the naval manager will train now more appropriate ships. - let already = false; - let shipToSwitch; - gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) { - if (already || !ship.hasClass("Trader")) - return; - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - { - already = true; - return; - } - shipToSwitch = ship; - }); - if (already) - return; - if (shipToSwitch) - { - if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) - shipToSwitch.setMetadata(PlayerID, "role", "trader"); - else - shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader"); - return; - } - - template = gameState.applyCiv("units/{civ}_ship_merchant"); - metadata.sea = this.tradeRoute.sea; - } - else - { - template = gameState.applyCiv("units/{civ}_support_trader"); - if (!this.tradeRoute.source.hasClass("NavalMarket")) - metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base"); - else - metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base"); - } - - if (!gameState.getTemplate(template)) - { - if (this.Config.debug > 0) - API3.warn("Petra error: trying to train " + template + " for civ " + - gameState.getPlayerCiv() + " but no template found."); - return; - } - queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1)); -}; - -m.TradeManager.prototype.updateTrader = function(gameState, ent) -{ - if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && - !ent.unitAIState().startsWith("INDIVIDUAL.GATHER") && - m.gatherTreasure(gameState, ent, true)) - return; - - if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) - return; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; - - // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain - - Engine.ProfileStart("Trade Manager"); - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let route = this.checkRoutes(gameState, access); - if (!route) - { - // TODO try to garrison land trader inside merchant ship when only sea routes available - if (this.Config.debug > 0) - API3.warn(" no available route for " + ent.genericName() + " " + ent.id()); - Engine.ProfileStop(); - return; - } - - let nearerSource = true; - if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position())) - nearerSource = false; - - if (!ent.hasClass("Ship") && route.land != access) - { - if (nearerSource) - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position()); - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position()); - Engine.ProfileStop(); - return; - } - - if (nearerSource) - ent.tradeRoute(route.target, route.source); - else - ent.tradeRoute(route.source, route.target); - ent.setMetadata(PlayerID, "route", this.routeEntToId(route)); - Engine.ProfileStop(); -}; - -m.TradeManager.prototype.setTradingGoods = function(gameState) -{ - let tradingGoods = {}; - for (let res of Resources.GetCodes()) - tradingGoods[res] = 0; - // first, try to anticipate future needs - let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); - let remaining = 100; - let targetNum = this.Config.Economy.targetNumTraders; - for (let res in stocks) - { - if (res == "food") - continue; - let wantedRate = wantedRates[res]; - if (stocks[res] < 200) - { - tradingGoods[res] = wantedRate > 0 ? 20 : 10; - targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); - } - else if (stocks[res] < 500) - { - tradingGoods[res] = wantedRate > 0 ? 15 : 10; - targetNum += 2; - } - else if (stocks[res] < 1000) - { - tradingGoods[res] = 10; - targetNum += 1; - } - remaining -= tradingGoods[res]; - } - this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); - - - // then add what is needed now - let mainNeed = Math.floor(remaining * 70 / 100); - let nextNeed = remaining - mainNeed; - - tradingGoods[mostNeeded[0].type] += mainNeed; - if (mostNeeded[1].wanted > 0) - tradingGoods[mostNeeded[1].type] += nextNeed; - else - tradingGoods[mostNeeded[0].type] += nextNeed; - Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); - if (this.Config.debug > 2) - API3.warn(" trading goods set to " + uneval(tradingGoods)); -}; - -/** - * Try to barter unneeded resources for needed resources. - * only once per turn because the info is not updated within a turn - */ -m.TradeManager.prototype.performBarter = function(gameState) -{ - let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); - if (barterers.length == 0) - return false; - - // Available resources after account substraction - let available = gameState.ai.queueManager.getAvailableResources(gameState); - let needs = gameState.ai.queueManager.currentNeeds(gameState); - - let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); - - let barterPrices = gameState.getBarterPrices(); - // calculates conversion rates - let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); - - // loop through each missing resource checking if we could barter and help finishing a queue quickly. - for (let buy of Resources.GetCodes()) - { - // 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 (let sell of Resources.GetCodes()) - { - 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; - } - - let barterRate = getBarterRate(barterPrices, buy, sell); - if (barterRate > bestRate && barterRate > barterRateMin) - { - bestRate = barterRate; - bestToSell = sell; - } - } - if (bestToSell !== undefined) - { - let amount = available[bestToSell] > 5000 ? 500 : 100; - barterers[0].barter(buy, bestToSell, amount); - if (this.Config.debug > 2) - API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + - " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + - " rate buy " + rates[buy] + " available sell " + available[bestToSell] + - " available buy " + available[buy] + " barterRate " + bestRate + - " 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) - return false; - let bestToBuy; - let bestChoice = 0; - for (let buy of Resources.GetCodes()) - { - if (buy == "food") - continue; - let barterRateMin = 80; - if (available[buy] < 5000 && available.food > 5000) - barterRateMin -= 20 - Math.floor(available[buy]/250); - let barterRate = getBarterRate(barterPrices, buy, "food"); - if (barterRate < barterRateMin) - continue; - let choice = barterRate / (100 + available[buy]); - if (choice > bestChoice) - { - bestChoice = choice; - bestToBuy = buy; - } - } - if (bestToBuy !== undefined) - { - let amount = available.food > 5000 ? 500 : 100; - barterers[0].barter(bestToBuy, "food", amount); - if (this.Config.debug > 2) - API3.warn("Contingency bartering: sold food for " + bestToBuy + - " available sell " + available.food + " available buy " + available[bestToBuy] + - " barterRate " + getBarterRate(barterPrices, bestToBuy, "food") + - " amount " + amount); - return true; - } - - return false; -}; - -m.TradeManager.prototype.checkEvents = function(gameState, events) -{ - // check if one market from a traderoute is renamed, change the route accordingly - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.hasClass("Market")) - continue; - for (let trader of this.traders.values()) - { - let route = trader.getMetadata(PlayerID, "route"); - if (!route) - continue; - if (route.source == evt.entity) - route.source = evt.newentity; - else if (route.target == evt.entity) - route.target = evt.newentity; - else - continue; - trader.setMetadata(PlayerID, "route", route); - } - } - - // if one market (or market-foundation) is destroyed, we should look for a better route - for (let evt of events.Destroy) - { - if (!evt.entityObj) - continue; - let ent = evt.entityObj; - if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - // same thing if one market is built - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || - !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - - // and same thing for captured markets - for (let evt of events.OwnershipChanged) - { - if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) - continue; - this.activateProspection(gameState); - return true; - } - - // or if diplomacy changed - if (events.DiplomacyChanged.length) - { - this.activateProspection(gameState); - return true; - } - - return false; -}; - -m.TradeManager.prototype.activateProspection = function(gameState) -{ - this.routeProspection = true; - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); -}; - -/** - * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute - * If an index is given, it returns the best route with this index or the best land route if index is a land index - */ -m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) -{ - let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); - let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); - if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon - { - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - return false; - } - - let onlyOurs = !market2.hasEntities(); - if (onlyOurs) - market2 = market1; - let candidate = { "gain": 0 }; - let potential = { "gain": 0 }; - let bestIndex = { "gain": 0 }; - let bestLand = { "gain": 0 }; - - let mapSize = gameState.sharedScript.mapSize; - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let m1 of market1.values()) - { - if (!m1.position()) - continue; - let access1 = m.getLandAccess(gameState, m1); - let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined; - for (let m2 of market2.values()) - { - if (onlyOurs && m1.id() >= m2.id()) - continue; - if (!m2.position()) - continue; - let access2 = m.getLandAccess(gameState, m2); - let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined; - let land = access1 == access2 ? access1 : undefined; - let sea = sea1 && sea1 == sea2 ? sea1 : undefined; - if (!land && !sea) - continue; - if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) - continue; - let gainMultiplier; - if (land && traderTemplatesGains.landGainMultiplier) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else if (sea && traderTemplatesGains.navalGainMultiplier) - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - else - continue; - let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize)); - if (gain < this.minimalGain) - continue; - if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) - { - if (accessIndex) - { - if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land") - { - if (gain < bestLand.gain) - continue; - bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - if (gain < candidate.gain) - continue; - candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - if (gain < potential.gain) - continue; - potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - - if (potential.gain < 1) - this.potentialTradeRoute = undefined; - else - this.potentialTradeRoute = potential; - - if (candidate.gain < 1) - { - if (this.Config.debug > 2) - API3.warn("no better trade route possible"); - this.tradeRoute = undefined; - return false; - } - - if (this.Config.debug > 1 && this.tradeRoute) - { - if (candidate.gain > this.tradeRoute.gain) - API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain); - } - else if (this.Config.debug > 1) - API3.warn("one trade route set with gain " + candidate.gain); - this.tradeRoute = candidate; - - if (this.Config.chat) - { - let owner = this.tradeRoute.source.owner(); - if (owner == PlayerID) - owner = this.tradeRoute.target.owner(); - if (owner != PlayerID && !this.warnedAllies[owner]) - { // Warn an ally that we have a trade route with him - m.chatNewTradeRoute(gameState, owner); - this.warnedAllies[owner] = true; - } - } - - if (accessIndex) - { - if (bestIndex.gain > 0) - return bestIndex; - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) - return bestLand; - return false; - } - return true; -}; - -/** Called when a market was built or destroyed, and checks if trader orders should be changed */ -m.TradeManager.prototype.checkTrader = function(gameState, ent) -{ - let presentRoute = ent.getMetadata(PlayerID, "route"); - if (!presentRoute) - return; - - if (!ent.position()) - { - // This trader is garrisoned, we will decide later (when ungarrisoning) what to do - ent.setMetadata(PlayerID, "route", undefined); - return; - } - - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let possibleRoute = this.checkRoutes(gameState, access); - // Warning: presentRoute is from metadata, so contains entity ids - if (!possibleRoute || - possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target || - possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target) - { - // Trader will be assigned in updateTrader - ent.setMetadata(PlayerID, "route", undefined); - if (!possibleRoute && !ent.hasClass("Ship")) - { - let closestBase = m.getBestBase(gameState, ent, true); - if (closestBase.accessIndex == access) - { - let closestBasePos = closestBase.anchor.position(); - ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); - return; - } - } - ent.stopMoving(); - } -}; - -m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) -{ - if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market")) - return; - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) - return; - if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() && - !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities()) - return; - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market")); - if (!template) - return; - this.checkRoutes(gameState); - let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); - if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain - { // no position found - if (gameState.getOwnEntitiesByClass("BarterMarket", 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) - API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + - marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); - else - API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + - marketPos[3]); - } - - if (!this.tradeRoute) - gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - if (!this.tradeRoute) - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) -{ - if (expectedGain < this.minimalGain) - return false; - if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && - expectedGain < this.potentialTradeRoute.gain + 20) - return false; - return true; -}; - -m.TradeManager.prototype.update = function(gameState, events, queues) -{ - if (gameState.ai.HQ.canBarter) - this.performBarter(gameState); - - if (this.Config.difficulty <= 1) - return; - - if (this.checkEvents(gameState, events)) // true if one market was built or destroyed - { - this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); - this.checkRoutes(gameState); - } - - if (this.tradeRoute) - { - this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); - if (gameState.ai.playedTurn % 5 == 0) - this.trainMoreTraders(gameState, queues); - if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) - gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); - if (gameState.ai.playedTurn % 60 == 0) - this.setTradingGoods(gameState); - } - - if (this.routeProspection) - this.prospectForNewMarket(gameState, queues); -}; - -m.TradeManager.prototype.routeEntToId = function(route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - if (!route[key]) - return undefined; - ret[key] = route[key].id(); - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.routeIdToEnt = function(gameState, route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - ret[key] = gameState.getEntityById(route[key]); - if (!ret[key]) - return undefined; - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.Serialize = function() -{ - return { - "tradeRoute": this.routeEntToId(this.tradeRoute), - "potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute), - "routeProspection": this.routeProspection, - "targetNumTraders": this.targetNumTraders, - "warnedAllies": this.warnedAllies - }; -}; - -m.TradeManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - { - if (key == "tradeRoute" || key == "potentialTradeRoute") - this[key] = this.routeIdToEnt(gameState, data[key]); - else - this[key] = data[key]; - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/transportPlan.js b/install/petraBased/petra-single-base/transportPlan.js deleted file mode 100644 index ee33a15..0000000 --- a/install/petraBased/petra-single-base/transportPlan.js +++ /dev/null @@ -1,729 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Describes a transport plan - * Constructor assign units (units is an ID array), a destination (position). - * The naval manager will try to deal with it accordingly. - * - * By this I mean that the naval manager will find how to go from access point 1 to access point 2 - * and then carry units from there. - * - * Note: only assign it units currently over land, or it won't work. - * Also: destination should probably be land, otherwise the units will be lost at sea. - * - * metadata for units: - * transport = this.ID - * onBoard = ship.id() when affected to a ship but not yet garrisoned - * = "onBoard" when garrisoned in a ship - * = undefined otherwise - * endPos = position of destination - * - * metadata for ships - * transporter = this.ID - */ - -m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) -{ - this.ID = gameState.ai.uniqueIDs.transports++; - this.debug = gameState.ai.Config.debug; - this.flotilla = false; // when false, only one ship per transport ... not yet tested when true - - this.endPos = endPos; - this.endIndex = endIndex; - this.startIndex = startIndex; - // TODO only cases with land-sea-land are allowed for the moment - // we could also have land-sea-land-sea-land - if (startIndex == 1) - { - // special transport from already garrisoned ship - if (!ship) - { - this.failed = true; - return false; - } - this.sea = ship.getMetadata(PlayerID, "sea"); - ship.setMetadata(PlayerID, "transporter", this.ID); - ship.setStance("none"); - for (let 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) - API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); - return false; - } - } - - for (let ent of units) - { - ent.setMetadata(PlayerID, "transport", this.ID); - ent.setMetadata(PlayerID, "endPos", endPos); - } - - if (this.debug > 1) - API3.warn("Starting a new transport plan with ID " + this.ID + - " to index " + endIndex + " with units length " + units.length); - - this.state = "boarding"; - this.boardingPos = {}; - this.needTransportShips = ship === undefined; - this.nTry = {}; - return true; -}; - -m.TransportPlan.prototype.init = function(gameState) -{ - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "transport", this.ID)); - this.ships = gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - - this.units.registerUpdates(); - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - - this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range -}; - -/** count available slots */ -m.TransportPlan.prototype.countFreeSlots = function() -{ - let slots = 0; - for (let ship of this.transportShips.values()) - slots += this.countFreeSlotsOnShip(ship); - return slots; -}; - -m.TransportPlan.prototype.countFreeSlotsOnShip = function(ship) -{ - if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) - return 0; - let occupied = ship.garrisoned().length + - this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", ship.id())).length; - return Math.max(ship.garrisonMax() - occupied, 0); -}; - -m.TransportPlan.prototype.assignUnitToShip = function(gameState, ent) -{ - if (this.needTransportShips) - return; - - for (let ship of this.transportShips.values()) - { - if (this.countFreeSlotsOnShip(ship) == 0) - continue; - ent.setMetadata(PlayerID, "onBoard", ship.id()); - if (this.debug > 1) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - } - return; - } - - if (this.flotilla) - { - this.needTransportShips = true; - return; - } - - if (!this.needSplit) - this.needSplit = [ent]; - else - this.needSplit.push(ent); -}; - -m.TransportPlan.prototype.assignShip = function(gameState) -{ - let pos; - // choose a unit of this plan not yet assigned to a ship - for (let ent of this.units.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "onBoard") !== undefined) - continue; - pos = ent.position(); - break; - } - // and choose the nearest available ship from this unit - let distmin = Math.min(); - let nearest; - gameState.ai.HQ.navalManager.seaTransportShips[this.sea].forEach(ship => { - if (ship.getMetadata(PlayerID, "transporter")) - return; - if (pos) - { - let dist = API3.SquareVectorDistance(pos, ship.position()); - if (dist > distmin) - return; - distmin = dist; - nearest = ship; - } - else if (!nearest) - nearest = ship; - }); - if (!nearest) - return false; - - nearest.setMetadata(PlayerID, "transporter", this.ID); - nearest.setStance("none"); - this.ships.updateEnt(nearest); - this.transportShips.updateEnt(nearest); - this.needTransportShips = false; - return true; -}; - -/** add a unit to this plan */ -m.TransportPlan.prototype.addUnit = function(unit, endPos) -{ - unit.setMetadata(PlayerID, "transport", this.ID); - unit.setMetadata(PlayerID, "endPos", endPos); - this.units.updateEnt(unit); -}; - -/** remove a unit from this plan, if not yet on board */ -m.TransportPlan.prototype.removeUnit = function(gameState, unit) -{ - let 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); - let ship = gameState.getEntityById(shipId); - if (ship && !ship.garrisoned().length && - !this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", shipId)).length) - { - this.releaseShip(ship); - this.ships.updateEnt(ship); - this.transportShips.updateEnt(ship); - } - } -}; - -m.TransportPlan.prototype.releaseShip = function(ship) -{ - if (ship.getMetadata(PlayerID, "transporter") != this.ID) - { - API3.warn(" Petra: try removing a transporter ship with " + ship.getMetadata(PlayerID, "transporter") + - " from " + this.ID + " and stance " + ship.getStance()); - return; - } - - let defaultStance = ship.get("UnitAI/DefaultStance"); - if (defaultStance) - ship.setStance(defaultStance); - - ship.setMetadata(PlayerID, "transporter", undefined); - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - ship.setMetadata(PlayerID, "role", "trader"); -}; - -m.TransportPlan.prototype.releaseAll = function() -{ - for (let ship of this.ships.values()) - this.releaseShip(ship); - - for (let 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 */ -m.TransportPlan.prototype.cancelTransport = function(gameState) -{ - let ent = this.units.toEntityArray()[0]; - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchor || !base.anchor.position()) - { - for (let newbase of gameState.ai.HQ.baseManagers) - { - if (!newbase.anchor || !newbase.anchor.position()) - continue; - ent.setMetadata(PlayerID, "base", newbase.ID); - base = newbase; - break; - } - if (!base.anchor || !base.anchor.position()) - return false; - this.units.forEach(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. There are two states: - * - "boarding" means we're trying to board units onto our ships - * - "sailing" means we're moving ships and eventually unload units - * - then the plan is cleared - */ - -m.TransportPlan.prototype.update = function(gameState) -{ - if (this.state == "boarding") - this.onBoarding(gameState); - else if (this.state == "sailing") - this.onSailing(gameState); - - return this.units.length; -}; - -m.TransportPlan.prototype.onBoarding = function(gameState) -{ - let ready = true; - let time = gameState.ai.elapsedTime; - let shipTested = {}; - - for (let ent of this.units.values()) - { - if (!ent.getMetadata(PlayerID, "onBoard")) - { - ready = false; - this.assignUnitToShip(gameState, ent); - if (ent.getMetadata(PlayerID, "onBoard")) - { - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let 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; - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let ship = gameState.getEntityById(shipId); - if (!ship) // the ship must have been destroyed - { - ent.setMetadata(PlayerID, "onBoard", undefined); - continue; - } - let distShip = API3.SquareVectorDistance(this.boardingPos[shipId], ship.position()); - if (!shipTested[shipId] && distShip > this.boardingRange) - { - shipTested[shipId] = true; - let retry = false; - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.WALKING" || - unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") - { - if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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, 30); - ent.garrison(ship, true); - } - else if (API3.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 (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.state = "sailing"; - this.nTry = {}; - this.unloaded = []; - this.recovered = []; -}; - -/** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ -m.TransportPlan.prototype.isOnBoard = function(ent) -{ - for (let ship of this.transportShips.values()) - { - if (ship.garrisoned().indexOf(ent.id()) == -1) - continue; - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - return true; - } - return false; -}; - -/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ -m.TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, avoidEnnemy) -{ - if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex); - return destination; - } - else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); - return destination; - } - - let startPos = ship.position(); - let distmin = Math.min(); - let posmin = destination; - let width = gameState.getPassabilityMap().width; - let cell = gameState.getPassabilityMap().cellSize; - let alliedDocks = gameState.getAllyStructures().filter(API3.Filters.and( - API3.Filters.byClass("Dock"), API3.Filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); - for (let i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - let pos = [i%width+0.5, Math.floor(i/width)+0.5]; - pos = [cell*pos[0], cell*pos[1]]; - let dist = API3.VectorDistance(startPos, pos); - if (destination) - dist += API3.VectorDistance(pos, destination); - if (avoidEnnemy) - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - dist += 100000000; - } - // require a small distance between all ships of the transport plan to avoid path finder problems - // this is also used when the ship is blocked and we want to find a new boarding point - for (let shipId in this.boardingPos) - if (this.boardingPos[shipId] !== undefined && - API3.SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) - dist += 1000000; - // and not too near our allied docks to not disturb naval traffic - let distSquare; - for (let dock of alliedDocks) - { - if (dock.foundationProgress() !== undefined) - distSquare = 900; - else - distSquare = 4900; - let dockDist = API3.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; -}; - -m.TransportPlan.prototype.onSailing = function(gameState) -{ - // Check that the units recovered on the previous turn have been reloaded - for (let recov of this.recovered) - { - let ent = gameState.getEntityById(recov.entId); - if (!ent) // entity destroyed - continue; - if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again - { - let ship = gameState.getEntityById(recov.shipId); - if (ship) - ship.moveApart(recov.entPos, 15); - continue; - } - if (this.debug > 1) - API3.warn(">>> transport " + this.ID + " reloading failed ... <<<"); - // destroy the unit if inaccessible otherwise leave it there - let index = m.getLandAccess(gameState, ent); - if (gameState.ai.HQ.landRegions[index]) - { - if (this.debug > 1) - API3.warn(" recovered entity kept " + ent.id()); - this.resetUnit(gameState, ent); - // TODO we should not destroy it, but now the unit could still be reloaded on the next turn - // and mess everything - ent.destroy(); - } - else - { - if (this.debug > 1) - API3.warn("recovered entity destroyed " + ent.id()); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - this.recovered = []; - - // Check that the units unloaded on the previous turn have been really unloaded and in the right position - let shipsToMove = {}; - for (let entId of this.unloaded) - { - let ent = gameState.getEntityById(entId); - if (!ent) // entity destroyed - continue; - else if (!ent.position()) // unloading failed - { - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship) - { - if (ship.garrisoned().indexOf(entId) != -1) - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - else - { - API3.warn("Petra transportPlan problem: unit not on ship without position ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - API3.warn("Petra transportPlan problem: unit on ship, but no ship ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else if (m.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) - API3.warn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship && !this.canceled) - { - shipsToMove[ship.id()] = ship; - this.recovered.push({ "entId": ent.id(), "entPos": ent.position(), "shipId": ship.id() }); - ent.garrison(ship); - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - } - else - { - if (this.debug > 1) - API3.warn("no way ... we destroy it"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - // And make some room for other units - let pos = ent.position(); - let goal = ent.getMetadata(PlayerID, "endPos"); - let dist = goal ? API3.VectorDistance(pos, goal) : 0; - if (dist > 30) - ent.moveToRange(goal[0], goal[1], dist-20, dist-20); - else - ent.moveToRange(pos[0], pos[1], 20, 20); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - } - } - for (let shipId in shipsToMove) - { - this.boardingPos[shipId] = this.getBoardingPos(gameState, shipsToMove[shipId], this.endIndex, this.sea, this.endPos, true); - shipsToMove[shipId].move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - this.unloaded = []; - - if (this.canceled) - { - for (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.canceled = undefined; - } - - for (let ship of this.transportShips.values()) - { - if (ship.unitAIState() == "INDIVIDUAL.WALKING") - continue; - let shipId = ship.id(); - let dist = API3.SquareVectorDistance(ship.position(), this.boardingPos[shipId]); - let remaining = 0; - for (let entId of ship.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (!ent.getMetadata(PlayerID, "transport")) - continue; - remaining++; - if (dist < 625) - { - ship.unload(entId); - this.unloaded.push(entId); - ent.setMetadata(PlayerID, "onBoard", shipId); - } - } - - let recovering = 0; - for (let recov of this.recovered) - if (recov.shipId == shipId) - recovering++; - - if (!remaining && !recovering) // when empty, release the ship and move apart to leave room for other ships. TODO fight - { - ship.moveApart(this.boardingPos[shipId], 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) - API3.warn(shipId + " new attempt for a landing point "); - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); - } - ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - } -}; - -m.TransportPlan.prototype.resetUnit = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // if from an army or attack, remove it - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - if (ent.getMetadata(PlayerID, "PartOfArmy")) - { - let army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); - if (army) - army.removeOwn(gameState, ent.id()); - } -}; - -m.TransportPlan.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "flotilla": this.flotilla, - "endPos": this.endPos, - "endIndex": this.endIndex, - "startIndex": this.startIndex, - "sea": this.sea, - "state": this.state, - "boardingPos": this.boardingPos, - "needTransportShips": this.needTransportShips, - "nTry": this.nTry, - "canceled": this.canceled, - "unloaded": this.unloaded, - "recovered": this.recovered - }; -}; - -m.TransportPlan.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; - - this.failed = false; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/victoryManager.js b/install/petraBased/petra-single-base/victoryManager.js deleted file mode 100644 index e4a0453..0000000 --- a/install/petraBased/petra-single-base/victoryManager.js +++ /dev/null @@ -1,748 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Handle events that are important to specific victory conditions: - * in capture_the_relic, capture gaia relics and train military guards. - * in regicide, train healer and military guards for the hero. - * in wonder, train military guards. - */ - -m.VictoryManager = function(Config) -{ - 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. - */ -m.VictoryManager.prototype.init = function(gameState) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) - this.criticalEnts.set(wonder.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let hero of gameState.getOwnEntitiesByClass("Hero", true).values()) - { - let defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; - if (hero.getStance() != defaultStance) - hero.setStance(defaultStance); - this.criticalEnts.set(hero.id(), { - "garrisonEmergency": false, - "healersAssigned": 0, - "guardsAssigned": 0, // for non-healer guards - "guards": new Map() // ids of ents who are currently guarding this hero - }); - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - for (let relic of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).values()) - { - if (relic.owner() == PlayerID) - this.criticalEnts.set(relic.id(), { "guardsAssigned": 0, "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 - */ -m.VictoryManager.prototype.checkEvents = function(gameState, events) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let evt of events.Create) - { - let 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. - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - let builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); - if (builders) - for (let worker of builders.values()) - { - worker.setMetadata(PlayerID, "base", base.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - } - } - - for (let evt of events.ConstructionFinished) - { - if (!evt || !evt.newentity) - continue; - - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) - this.criticalEnts.set(ent.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let evt of events.Attacked) - { - if (!this.criticalEnts.has(evt.target)) - continue; - - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) - continue; - - let plan = target.getMetadata(PlayerID, "plan"); - let hero = this.criticalEnts.get(evt.target); - if (plan != -2 && plan != -3) - { - target.stopMoving(); - - if (plan >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); - if (attackPlan) - attackPlan.removeUnit(target, true); - } - - if (target.getMetadata(PlayerID, "PartOfArmy")) - { - let 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 (let evt of events.TrainingFinished) - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") == "criticalEntHealer") - this.assignGuardToCriticalEnt(gameState, ent); - } - - for (let evt of events.Garrison) - { - if (!this.criticalEnts.has(evt.entity)) - continue; - - let hero = this.criticalEnts.get(evt.entity); - if (hero.garrisonEmergency) - hero.garrisonEmergency = false; - - let holderEnt = gameState.getEntityById(evt.holder); - if (!holderEnt) - continue; - - if (holderEnt.hasClass("Ship")) - { - // If the hero is garrisoned on a ship, remove its guards - for (let guardId of hero.guards.keys()) - { - let 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 (let guardId of hero.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt) - continue; - - let 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; - - let pos = holderEnt.position(); - let radius = holderEnt.obstructionRadius().max; - if (pos) - guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); - } - } - } - - for (let evt of events.EntityRenamed) - { - if (!this.guardEnts.has(evt.entity)) - continue; - for (let 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 (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() != PlayerID) - continue; - - let entId = evt.entityObj.id(); - if (this.criticalEnts.has(entId)) - { - this.removeCriticalEnt(gameState, entId); - continue; - } - - if (!this.guardEnts.has(entId)) - continue; - - for (let data of this.criticalEnts.values()) - if (data.guards.has(entId)) - { - data.guards.delete(entId); - if (evt.entityObj.hasClass("Healer")) - --data.healersAssigned; - else - --data.guardsAssigned; - break; - } - - this.guardEnts.delete(entId); - } - - for (let evt of events.UnGarrison) - { - if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) - continue; - - let 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") == "criticalEntHealer" || - ent.getMetadata(PlayerID, "role") == "criticalEntGuard") && !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 - let criticalEnt = this.criticalEnts.get(evt.entity); - for (let [id, isGuarding] of this.guardEnts) - { - if (criticalEnt.guards.size >= this.healersPerCriticalEnt) - break; - - if (!isGuarding) - { - let guardEnt = gameState.getEntityById(id); - if (guardEnt) - this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); - } - } - } - - for (let 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; - - let 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": 0, "guards": new Map() }); - // Move captured relics to the closest base - if (ent.hasClass("Relic")) - this.pickCriticalEntRetreatLocation(gameState, ent, false); - } - } -}; - -m.VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId) -{ - for (let [guardId, role] of this.criticalEnts.get(criticalEntId).guards) - { - let 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 - */ -m.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 (let data of this.criticalEnts.values()) - { - if (data.healersAssigned === undefined || data.healersAssigned >= this.healersPerCriticalEnt) - continue; - let template = gameState.applyCiv("units/{civ}_support_healer_b"); - queues.healer.addPlan(new m.TrainingPlan(gameState, template, { "role": "criticalEntHealer", "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. - */ -m.VictoryManager.prototype.manageCriticalEntGuards = function(gameState) -{ - let numWorkers = gameState.getOwnEntitiesByRole("worker", true).length; - if (numWorkers < 20) - { - for (let data of this.criticalEnts.values()) - { - for (let guardId of data.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || - guardEnt.getMetadata(PlayerID, "role") != "criticalEntGuard") - continue; - - guardEnt.removeGuard(); - guardEnt.setMetadata(PlayerID, "plan", -1); - guardEnt.setMetadata(PlayerID, "role", undefined); - this.guardEnts.delete(guardId); - --data.guardsAssigned; - - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - - if (++numWorkers >= 20) - break; - } - if (numWorkers >= 20) - break; - } - } - - let minWorkers = 25; - let deltaWorkers = 3; - for (let [id, data] of this.criticalEnts) - { - let criticalEnt = gameState.getEntityById(id); - if (!criticalEnt) - continue; - - let militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + - Math.round(this.Config.personality.defensive * 5); - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt) - continue; - - // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports - for (let checkForSameAccess of [true, false]) - { - // First try to assign any Champion units we might have - for (let entity of gameState.getOwnEntitiesByClass("Champion", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.ai.HQ.attackManager.outOfPlan.values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - } -}; - -m.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() || - m.getLandAccess(gameState, criticalEnt) != m.getLandAccess(gameState, guardEnt))) - return false; - - if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) - return false; - - guardEnt.setMetadata(PlayerID, "plan", -2); - guardEnt.setMetadata(PlayerID, "role", "criticalEntGuard"); - return true; -}; - -m.VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency) -{ - gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); - let 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"); - let accessIndex = m.getLandAccess(gameState, criticalEnt); - let bestBase = m.getBestBase(gameState, criticalEnt, true); - if (bestBase.accessIndex == accessIndex) - { - let bestBasePos = bestBase.anchor.position(); - criticalEnt.move(bestBasePos[0], bestBasePos[1]); - } -}; - -/** - * 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). - */ -m.VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId) -{ - if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) - return false; - - if (criticalEntId && !this.criticalEnts.has(criticalEntId)) - { - criticalEntId = undefined; - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - } - - if (!criticalEntId) - { - let isHealer = guardEnt.hasClass("Healer"); - - // Assign to the critical ent with the fewest guards - let min = Math.min(); - for (let [id, data] of this.criticalEnts) - { - if (isHealer && (data.healersAssigned === undefined || data.healersAssigned > min)) - continue; - if (!isHealer && data.guardsAssigned > min) - continue; - - criticalEntId = id; - min = isHealer ? data.healersAssigned : data.guardsAssigned; - } - if (criticalEntId) - { - let data = this.criticalEnts.get(criticalEntId); - if (isHealer) - ++data.healersAssigned; - else - ++data.guardsAssigned; - } - } - - if (!criticalEntId) - { - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - return false; - } - - let 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); - - let guardEntAccess = m.getLandAccess(gameState, guardEnt); - let criticalEntAccess = m.getLandAccess(gameState, criticalEnt); - if (guardEntAccess == criticalEntAccess) - { - let queued = m.returnResources(gameState, guardEnt); - guardEnt.guard(criticalEnt, queued); - let guardRole = guardEnt.getMetadata(PlayerID, "role") == "criticalEntHealer" ? "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; -}; - -m.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; -}; - -m.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 (let id of this.criticalEnts.keys()) - { - let 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 (let relicId of this.targetedGaiaRelics.keys()) - { - let 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 - let allGaiaRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == 0); - for (let relic of allGaiaRelics.values()) - { - let relicPosition = relic.position(); - if (!relicPosition || this.targetedGaiaRelics.has(relic.id())) - continue; - let 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 (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position() || !ent.visionRange()) - continue; - if (API3.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. - */ -m.VictoryManager.prototype.captureGaiaRelic = function(gameState, relic) -{ - let capture = -relic.defaultRegenRate(); - let sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b); - let plans = this.targetedGaiaRelics.get(relic.id()); - for (let plan of plans) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (!attack) - continue; - for (let ent of attack.unitCollection.values()) - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - } - // No need to make a new attack if already enough units - if (capture > sumCapturePoints / 50) - return; - let relicPosition = relic.position(); - let access = m.getLandAccess(gameState, relic); - let 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; - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan == -2 || plan == -3) - return false; - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && (attack.state != "unexecuted" || attack.type == "Raid")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(relicPosition); - let expedition = []; - for (let ent of units.values()) - { - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - expedition.push(ent); - if (capture > sumCapturePoints / 25) - break; - } - if (!expedition.length || !plans.length && capture < sumCapturePoints / 100) - return; - let attack = gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); - if (!attack) - return; - let plan = attack.name; - attack.rallyPoint = undefined; - for (let 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); -}; - -m.VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId) -{ - for (let plan of this.targetedGaiaRelics.get(relicId)) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack) - attack.Abort(gameState); - } - this.targetedGaiaRelics.delete(relicId); -}; - -m.VictoryManager.prototype.Serialize = function() -{ - return { - "criticalEnts": this.criticalEnts, - "guardEnts": this.guardEnts, - "healersPerCriticalEnt": this.healersPerCriticalEnt, - "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, - "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, - "targetedGaiaRelics": this.targetedGaiaRelics - }; -}; - -m.VictoryManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-single-base/worker.js b/install/petraBased/petra-single-base/worker.js deleted file mode 100644 index bb4e2b5..0000000 --- a/install/petraBased/petra-single-base/worker.js +++ /dev/null @@ -1,1114 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This class makes a worker do as instructed by the economy manager - */ - -m.Worker = function(base) -{ - this.ent = undefined; - this.base = base; - this.baseID = base.ID; -}; - -m.Worker.prototype.update = function(gameState, ent) -{ - if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return; - - let subrole = ent.getMetadata(PlayerID, "subrole"); - - // If we are waiting for a transport or we are sailing, just wait - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board - if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target && plan && plan.state == "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 == "gatherer" || subrole == "hunter") - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan.state == "boarding" && ent.position()) - { - let hasDropsite = false; - let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; - for (let structure of gameState.getOwnStructures().values()) - { - if (m.getLandAccess(gameState, structure) != plan.endIndex) - continue; - let resourceDropsiteTypes = m.getBuiltEntity(gameState, structure).resourceDropsiteTypes(); - if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) - continue; - hasDropsite = true; - break; - } - if (!hasDropsite) - { - for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values()) - { - if (!unit.position() || m.getLandAccess(gameState, unit) != plan.endIndex) - continue; - let resourceDropsiteTypes = unit.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 = m.getLandAccess(gameState, ent); - // base 0 for unassigned entities has no accessIndex, so take the one from the entity - if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) - this.baseAccess = this.entAccess; - else - this.baseAccess = this.base.accessIndex; - - if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... - { - ent.setMetadata(PlayerID, "subrole", "idle"); - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - - this.ent = ent; - - let unitAIState = ent.unitAIState(); - if ((subrole == "hunter" || subrole == "gatherer") && - (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) - { - if (this.isInaccessibleSupply(gameState)) - { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); - } - - if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - // Check that we have not drifted too far when hunting - let target = gameState.getEntityById(orderData.target); - if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") - { - let 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)) - { - let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000; - let targetAccess = m.getLandAccess(gameState, target); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - let hasFoodDropsiteWithinDistance = false; - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (targetAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.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); - } - - let 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 == "fisher") - this.startFishing(gameState); - else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && - !ent.getMetadata(PlayerID, "PartOfArmy")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - let target = gameState.getEntityById(orderData.target); - if (target && (!target.position() || m.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")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") - { - // If we are here, an enemy structure must have targeted one of our workers - // and UnitAI sent it fight back with allowCapture=true - let target = gameState.getEntityById(orderData.target); - if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) - ent.attack(orderData.target, m.allowCapture(gameState, ent, target)); - } - } - return; - } - - // Okay so we have a few tasks. - // If we're gathering, we'll check that we haven't run idle. - // And we'll also check that we're gathering a resource we want to gather. - - if (subrole == "gatherer") - { - if (ent.isIdle()) - { - // if we aren't storing resources or it's the same type as what we're about to gather, - // let's just pick a new resource. - // TODO if we already carry the max we can -> returnresources - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || - ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) - { - this.startGathering(gameState); - } - else if (!m.returnResources(gameState, ent)) // try to deposit resources - { - // no dropsite, abandon old resources and start gathering new ones - this.startGathering(gameState); - } - } - else if (unitAIStateOrder == "GATHER") - { - // we're already gathering. But let's check if there is nothing better - // in case UnitAI did something bad - if (ent.unitAIOrderData().length) - { - let supplyId = ent.unitAIOrderData()[0].target; - let supply = gameState.getEntityById(supplyId); - if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") && - supply.resourceSupplyType().generic != "treasure" && - supplyId != ent.getMetadata(PlayerID, "supply")) - { - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); - if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (nearby.some(sup => sup.id == supplyId)) - ent.setMetadata(PlayerID, "supply", supplyId); - else if (nearby.length) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (medium.length && !medium.some(sup => sup.id == supplyId)) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - ent.setMetadata(PlayerID, "supply", supplyId); - } - } - } - } - } - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (gameState.ai.playedTurn % 10 == 0) - { - // Check from time to time that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, this.ent); - } - - // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. - // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be - // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; - if (influenceGroup && influenceGroup == "sparse") - { - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (supplyId) - { - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (!nearby.some(sup => sup.id == supplyId)) - { - if (nearby.length) - ent.setMetadata(PlayerID, "supply", undefined); - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (!medium.some(sup => sup.id == supplyId) && medium.length) - ent.setMetadata(PlayerID, "supply", undefined); - } - } - } - } - } - } - else if (subrole == "builder") - { - if (unitAIStateOrder == "REPAIR") - { - // Update our target in case UnitAI sent us to a different foundation because of autocontinue - // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) - if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && - ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) - { - let targetId = ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (target && !target.hasClass("Field")) - { - ent.setMetadata(PlayerID, "target-foundation", targetId); - return; - } - ent.setMetadata(PlayerID, "target-foundation", undefined); - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) - let targetId = ent.getMetadata(PlayerID, "target-foundation"); - if (targetId && gameState.getEntityById(targetId)) - return; - ent.stopMoving(); - } - // okay so apparently we aren't working. - // Unless we've been explicitely told to keep our role, make us idle. - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.setMetadata(PlayerID, "target-foundation", undefined); - // If worker elephant, move away to avoid being trapped in between constructions - if (ent.hasClass("Elephant")) - this.moveToGatherer(gameState, ent, true); - else if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - else - { - let goalAccess = m.getLandAccess(gameState, target); - let queued = m.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 == "hunter") - { - let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); - if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) - { - if (!this.startHunting(gameState)) - { - // nothing to hunt around. Try another region if any - let nowhereToHunt = true; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let basePos = base.anchor.position(); - if (this.startHunting(gameState, basePos)) - { - ent.setMetadata(PlayerID, "base", base.ID); - 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" || unitAIStateOrder == "RETURNRESOURCE") - { - // we may have drifted towards ennemy territory during the hunt, if yes go home - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startHunting(gameState); - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - // Check that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, ent); - } - } - } - } - else if (subrole == "fisher") - { - if (ent.isIdle()) - this.startFishing(gameState); - else // if we have drifted towards ennemy territory during the fishing, go home - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startFishing(gameState); - } - } -}; - -m.Worker.prototype.retryWorking = function(gameState, subrole) -{ - switch (subrole) - { - case "gatherer": - return this.startGathering(gameState); - case "hunter": - return this.startHunting(gameState); - case "fisher": - return this.startFishing(gameState); - case "builder": - return this.startBuilding(gameState); - default: - return false; - } -}; - -m.Worker.prototype.startBuilding = function(gameState) -{ - let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) - return false; - if (m.getLandAccess(gameState, target) != this.entAccess) - return false; - this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses - return true; -}; - -m.Worker.prototype.startGathering = function(gameState) -{ - // First look for possible treasure if any - if (m.gatherTreasure(gameState, this.ent)) - return true; - - let resource = this.ent.getMetadata(PlayerID, "gather-type"); - - // If we are gathering food, try to hunt first - if (resource == "food" && this.startHunting(gameState)) - return true; - - let findSupply = function(ent, supplies) { - let ret = false; - let gatherRates = ent.resourceGatherRates(); - for (let i = 0; i < supplies.length; ++i) - { - // exhausted resource, remove it from this list - if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) - { - supplies.splice(i--, 1); - continue; - } - if (m.IsSupplyFull(gameState, supplies[i].ent)) - continue; - let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - let supplyType = supplies[i].ent.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); - if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && - supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - // not in ennemy territory - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - continue; - gameState.ai.HQ.AddTCGatherer(supplies[i].id); - ent.setMetadata(PlayerID, "supply", supplies[i].id); - ret = supplies[i].ent; - break; - } - return ret; - }; - - let navalManager = gameState.ai.HQ.navalManager; - let supply; - - // first look in our own base if accessible from our present position - if (this.baseAccess == this.entAccess) - { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.gather(supply); - return true; - } - // --> for food, try to gather from fields if any, otherwise build one if any - if (resource == "food") - { - supply = this.gatherNearestField(gameState, this.baseID); - if (supply) - { - this.ent.gather(supply); - return true; - } - supply = this.buildAnyField(gameState, this.baseID); - if (supply) - { - this.ent.repair(supply); - return true; - } - } - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - // So if we're here we have checked our whole base for a proper resource (or it was not accessible) - // --> check other bases directly accessible - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any - { - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - - // Okay may-be we haven't found any appropriate dropsite anywhere. - // Try to help building one if any accessible foundation available - let foundations = gameState.getOwnFoundations().toEntityArray(); - let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { - if (!foundation || m.getLandAccess(gameState, foundation) != this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - if (foundation.getMetadata(PlayerID, "base") != this.baseID) - this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); - this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); - this.ent.setMetadata(PlayerID, "subrole", "builder"); - this.ent.repair(foundation); - return true; - } - return false; - }, this); - if (shouldBuild) - return true; - - // Still nothing ... try bases which need a transport - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 (let 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 || m.getLandAccess(gameState, foundation) == this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - let foundationAccess = m.getLandAccess(gameState, foundation); - if (navalManager.requireTransport(gameState, this.ent, 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", "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.ent, this.base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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) - API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -/** - * if position is given, we only check if we could hunt from this position but do nothing - * otherwise the position of the entity is taken, and if something is found, we directly start the hunt - */ -m.Worker.prototype.startHunting = function(gameState, position) -{ - // First look for possible treasure if any - if (!position && m.gatherTreasure(gameState, this.ent)) - return true; - - let resources = gameState.getHuntableSupplies(); - if (!resources.hasEntities()) - return false; - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let isCavalry = this.ent.hasClass("Cavalry"); - let isRanged = this.ent.hasClass("Ranged"); - let entPosition = position ? position : this.ent.position(); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - - let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) - { - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (supplyAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) - return true; - } - return false; - }; - - let gatherRates = this.ent.resourceGatherRates(); - for (let supply of resources.values()) - { - if (!supply.position()) - continue; - - let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - if (m.IsSupplyFull(gameState, supply)) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - - let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; - // Only cavalry and range units should hunt fleeing animals - if (canFlee && !isCavalry && !isRanged) - continue; - - let supplyAccess = m.getLandAccess(gameState, supply); - if (supplyAccess != this.entAccess) - continue; - - // measure the distance to the resource - let dist = API3.SquareVectorDistance(entPosition, supply.position()); - if (dist > nearestSupplyDist) - continue; - - // Only cavalry should hunt faraway - if (!isCavalry && dist > 25000) - continue; - - // Avoid ennemy territory - let 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 cavalry should hunt far from dropsite (specially for non domestic animals which flee) - if (!isCavalry && canFlee && territoryOwner == 0) - continue; - let distanceSquare = isCavalry ? 35000 : (canFlee ? 7000 : 12000); - if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) - continue; - - nearestSupplyDist = dist; - nearestSupply = supply; - } - - if (nearestSupply) - { - if (position) - return true; - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - return false; -}; - -m.Worker.prototype.startFishing = function(gameState) -{ - if (!this.ent.position()) - return false; - - let resources = gameState.getFishableSupplies(); - if (!resources.hasEntities()) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState); - this.ent.destroy(); - return false; - } - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let fisherSea = m.getSeaAccess(gameState, this.ent); - let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")). - filter(API3.Filters.byClass("Dock")).toEntityArray(); - - let nearestDropsiteDist = function(supply) { - let distMin = 1000000; - let pos = supply.position(); - for (let dropsite of fishDropsites) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (fisherSea != m.getSeaAccess(gameState, dropsite)) - continue; - distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); - } - return distMin; - }; - - let exhausted = true; - let gatherRates = this.ent.resourceGatherRates(); - resources.forEach(function(supply) - { - if (!supply.position()) - return; - - // check that it is accessible - if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) - return; - - exhausted = false; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - return; - - if (m.IsSupplyFull(gameState, supply)) - return; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - return; - - // Avoid ennemy territory - if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) - return; - - // measure the distance from the resource to the nearest dropsite - let dist = nearestDropsiteDist(supply); - if (dist > nearestSupplyDist) - return; - - nearestSupplyDist = dist; - nearestSupply = supply; - }); - - if (exhausted) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); - this.ent.destroy(); - return false; - } - - if (nearestSupply) - { - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - if (this.ent.getMetadata(PlayerID, "subrole") == "fisher") - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -m.Worker.prototype.gatherNearestField = function(gameState, baseID) -{ - let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); - let bestFarm; - - let gatherRates = this.ent.resourceGatherRates(); - for (let field of ownFields.values()) - { - if (m.IsSupplyFull(gameState, field)) - continue; - let supplyType = field.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - let rate = 1; - let diminishing = field.getDiminishingReturns(); - if (diminishing < 1) - { - let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); - if (num > 0) - rate = Math.pow(diminishing, num); - } - // Add a penalty distance depending on rate - let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; - if (!bestFarm || dist < bestFarm.dist) - bestFarm = { "ent": field, "dist": dist, "rate": rate }; - } - // If other field foundations available, better build them when rate becomes too small - if (!bestFarm || bestFarm.rate < 0.70 && - gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) - return false; - gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); - this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); - return bestFarm.ent; -}; - -/** - * WARNING with the present options of AI orders, the unit will not gather after building the farm. - * This is done by calling the gatherNearestField function when construction is completed. - */ -m.Worker.prototype.buildAnyField = function(gameState, baseID) -{ - if (!this.ent.isBuilder()) - return false; - let bestFarmEnt = false; - let bestFarmDist = 10000000; - let pos = this.ent.position(); - for (let found of gameState.getOwnFoundations().values()) - { - if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) - continue; - let current = found.getBuildersNb(); - if (current === undefined || - current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) - continue; - let dist = API3.SquareVectorDistance(found.position(), pos); - if (dist > bestFarmDist) - continue; - bestFarmEnt = found; - bestFarmDist = dist; - } - return bestFarmEnt; -}; - -/** - * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. - * For the time being, we move towards the nearest gatherer (providing him a dropsite). - * BaseManager does also use that function to deal with its mobile dropsites. - */ -m.Worker.prototype.moveToGatherer = function(gameState, ent, forced) -{ - let pos = ent.position(); - if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) - return; - let gatherers = this.base.workersBySubrole(gameState, "gatherer"); - let dist = Math.min(); - let destination; - let access = m.getLandAccess(gameState, ent); - let types = ent.resourceDropsiteTypes(); - for (let gatherer of gatherers.values()) - { - let gathererType = gatherer.getMetadata(PlayerID, "gather-type"); - if (!gathererType || types.indexOf(gathererType) == -1) - continue; - if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || - m.getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) - continue; - let distance = API3.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). - */ -m.Worker.prototype.isInaccessibleSupply = function(gameState) -{ - if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) - return false; - let targetId = this.ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (!target) - return true; - - if (!target.resourceSupplyType()) - return false; - - let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); - let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; - if (!approachingTarget || approachingTarget != targetId) - { - this.ent.setMetadata(PlayerID, "approachingTarget", targetId); - this.ent.setMetadata(PlayerID, "approachingTime", undefined); - this.ent.setMetadata(PlayerID, "approachingPos", undefined); - this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); - let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); - if (alreadyTried && alreadyTried != targetId) - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - } - - let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); - if (carriedBefore != carriedAmount) - { - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - if (target.getMetadata(PlayerID, "inaccessibleTime")) - target.setMetadata(PlayerID, "inaccessibleTime", 0); - return false; - } - - let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - return true; - - let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); - if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) - { - let presentPos = this.ent.position(); - let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); - if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) - { - this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); - this.ent.setMetadata(PlayerID, "approachingPos", presentPos); - return false; - } - if (gameState.ai.elapsedTime - approachingTime > 10) - { - if (this.ent.getMetadata(PlayerID, "alreadyTried")) - { - target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); - return true; - } - // let's try again to reach it - this.ent.setMetadata(PlayerID, "alreadyTried", targetId); - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.gather(target); - return false; - } - } - return false; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/_petrabot.js b/install/petraBased/petra-unitary/_petrabot.js deleted file mode 100644 index 3e5d861..0000000 --- a/install/petraBased/petra-unitary/_petrabot.js +++ /dev/null @@ -1,172 +0,0 @@ -Engine.IncludeModule("common-api"); - -var PETRA = (function() { -var m = {}; - -m.maxBaseCount = 1; - -m.PetraBot = function PetraBot(settings) -{ - API3.BaseAI.call(this, settings); - - 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 m.Config(settings.difficulty, settings.behavior); - - this.savedEvents = {}; -}; - -m.PetraBot.prototype = new API3.BaseAI(); - -m.PetraBot.prototype.CustomInit = function(gameState) -{ - if (this.isDeserialized) - { - // WARNING: the deserializations should not modify the metadatas infos inside their init functions - this.turn = this.data.turn; - this.playedTurn = this.data.playedTurn; - this.elapsedTime = this.data.elapsedTime; - this.savedEvents = this.data.savedEvents; - for (let key in this.savedEvents) - { - for (let i in this.savedEvents[key]) - { - if (!this.savedEvents[key][i].entityObj) - continue; - let evt = this.savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - { - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = new API3.Entity(gameState.sharedScript, evt.entityObj); - this.savedEvents[key][i] = evtmod; - } - } - } - - this.Config.Deserialize(this.data.config); - - this.queueManager = new m.QueueManager(this.Config, {}); - this.queueManager.Deserialize(gameState, this.data.queueManager); - this.queues = this.queueManager.queues; - - this.HQ = new m.HQ(this.Config); - 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 (let i in this.Config.priorities) - this.queues[i] = new m.Queue(); - - this.queueManager = new m.QueueManager(this.Config, this.queues); - - this.HQ = new m.HQ(this.Config); - - this.HQ.init(gameState, this.queues); - - // Analyze our starting position and set a strategy - this.HQ.gameAnalysis(gameState); - } -}; - -m.PetraBot.prototype.OnUpdate = function(sharedScript) -{ - if (this.gameFinished) - return; - - for (let i in this.events) - { - if (i == "AIMetadata") // not used inside petra - continue; - if(this.savedEvents[i] !== undefined) - this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]); - else - this.savedEvents[i] = this.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.gameState.getOwnEntities().length === 0) - { - Engine.ProfileStop(); - return; // With no entities to control the AI cannot do anything - } - - this.HQ.update(this.gameState, this.queues, this.savedEvents); - - this.queueManager.update(this.gameState); - - for (let i in this.savedEvents) - this.savedEvents[i] = []; - - Engine.ProfileStop(); - } - - this.turn++; -}; - -m.PetraBot.prototype.Serialize = function() -{ - let savedEvents = {}; - for (let key in this.savedEvents) - { - savedEvents[key] = this.savedEvents[key].slice(); - for (let i in savedEvents[key]) - { - if (!savedEvents[key][i].entityObj) - continue; - let evt = savedEvents[key][i]; - let evtmod = {}; - for (let keyevt in evt) - evtmod[keyevt] = evt[keyevt]; - evtmod.entityObj = evt.entityObj._entity; - savedEvents[key][i] = evtmod; - } - } - - return { - "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() - }; -}; - -m.PetraBot.prototype.Deserialize = function(data, sharedScript) -{ - this.isDeserialized = true; - this.data = data; -}; - -return m; -}()); diff --git a/install/petraBased/petra-unitary/attackManager.js b/install/petraBased/petra-unitary/attackManager.js deleted file mode 100644 index ebccbee..0000000 --- a/install/petraBased/petra-unitary/attackManager.js +++ /dev/null @@ -1,805 +0,0 @@ -var PETRA = function(m) -{ - -/** Attack Manager */ - -m.AttackManager = function(Config) -{ - this.Config = Config; - - this.totalNumber = 0; - this.attackNumber = 0; - this.rushNumber = 0; - this.raidNumber = 0; - this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; - this.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 = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.AttackManager.prototype.init = function(gameState) -{ - this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); - this.outOfPlan.registerUpdates(); -}; - -m.AttackManager.prototype.setRushes = function(allowed) -{ - if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) - { - this.maxRushes = 3; - this.rushSize = [ 16, 20, 24 ]; - } - 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 ]; - } -}; - -m.AttackManager.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.PlayerDefeated) - this.defeated[evt.playerId] = true; - - let answer = "decline"; - let other; - let targetPlayer; - for (let evt of events.AttackRequest) - { - if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) - continue; - targetPlayer = evt.player; - let available = 0; - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing") - { - if (attack.targetPlayer === targetPlayer) - available += attack.unitCollection.length; - else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) - other = attack.targetPlayer; - continue; - } - - attack.targetPlayer = targetPlayer; - - if (attack.unitCollection.length > 2) - available += attack.unitCollection.length; - } - } - - if (available > 12) // launch the attack immediately - { - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - { - if (attack.state === "completing" || - attack.targetPlayer !== targetPlayer || - attack.unitCollection.length < 3) - continue; - attack.forceStart(); - attack.requested = true; - } - } - answer = "join"; - } - else if (other !== undefined) - answer = "other"; - break; // take only the first attack request into account - } - if (targetPlayer !== undefined) - m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); - - for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks - { - for (let [targetId, unitIds] of this.bombingAttacks) - { - 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 - */ -m.AttackManager.prototype.assignBombers = function(gameState) -{ - // First some cleaning of current bombing attacks - for (let [targetId, unitIds] of this.bombingAttacks) - { - let target = gameState.getEntityById(targetId); - if (!target || !gameState.isPlayerEnemy(target.owner())) - this.bombingAttacks.delete(targetId); - else - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.owner() == PlayerID) - { - let plan = ent.getMetadata(PlayerID, "plan"); - let orders = ent.unitAIOrderData(); - let 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); - } - } - - let bombers = gameState.updatingCollection("bombers", API3.Filters.byClassesOr(["BoltShooter", "Catapult"]), gameState.getOwnUnits()); - for (let 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - continue; - } - let alreadyBombing = false; - for (let unitIds of this.bombingAttacks.values()) - { - if (!unitIds.has(ent.id())) - continue; - alreadyBombing = true; - break; - } - if (alreadyBombing) - break; - - let range = ent.attackRange("Ranged").max; - let entPos = ent.position(); - let access = m.getLandAccess(gameState, ent); - for (let struct of gameState.getEnemyStructures().values()) - { - let structPos = struct.position(); - let x; - let z; - if (struct.hasClass("Field")) - { - if (!struct.resourceSupplyNumGatherers() || - !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) - continue; - } - let dist = API3.VectorDistance(entPos, structPos); - if (dist > range) - { - let safety = struct.footprintRadius() + 30; - x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; - z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; - let 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 (let [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; - } - } -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.AttackManager.prototype.update = function(gameState, queues, events) -{ - if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) - { - this.debugTime = gameState.ai.elapsedTime; - API3.warn(" upcoming attacks ================="); - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" started attacks =================="); - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); - API3.warn(" =================================="); - } - - this.checkEvents(gameState, events); - - let unexecutedAttacks = { "Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0 }; - for (let attackType in this.upcomingAttacks) - { - for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) - { - let attack = this.upcomingAttacks[attackType][i]; - attack.checkEvents(gameState, events); - - if (attack.isStarted()) - API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); - - let updateStep = attack.updatePreparation(gameState); - // now we're gonna check if the preparation time is over - if (updateStep == 1 || attack.isPaused()) - { - // just chillin' - if (attack.state == "unexecuted") - ++unexecutedAttacks[attackType]; - } - else if (updateStep == 0) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - else if (updateStep == 2) - { - if (attack.StartAttack(gameState)) - { - if (this.Config.debug > 1) - API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); - if (this.Config.chat) - m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); - this.startedAttacks[attackType].push(attack); - } - else - attack.Abort(gameState); - this.upcomingAttacks[attackType].splice(i--, 1); - } - } - } - - for (let attackType in this.startedAttacks) - { - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - attack.checkEvents(gameState, events); - // okay so then we'll update the attack. - if (attack.isPaused()) - continue; - let remaining = attack.update(gameState, events); - if (!remaining) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } - } - - // creating plans after updating because an aborted plan might be reused in that case. - - let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; - if (this.rushNumber < this.maxRushes && barracksNb >= 1) - { - if (unexecutedAttacks.Rush === 0) - { - // we have a barracks and we want to rush, rush. - let data = { "targetSize": this.rushSize[this.rushNumber] }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); - if (!attackPlan.failed) - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Rush.push(attackPlan); - } - this.rushNumber++; - } - } - else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 && - this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) && - (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) - { - if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack - { - let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type); - if (attackPlan.failed) - this.attackPlansEncounteredWater = true; // hack - else - { - if (this.Config.debug > 1) - API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks[type].push(attackPlan); - } - this.attackNumber++; - } - } - - if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) - { - let target; - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - target = gameState.getEntityById(targetId); - if (!target) - 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 > 1 && gameState.ai.playedTurn % 5 == 0) - this.assignBombers(gameState); -}; - -m.AttackManager.prototype.getPlan = function(planName) -{ - for (let attackType in this.upcomingAttacks) - { - for (let attack of this.upcomingAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - for (let attackType in this.startedAttacks) - { - for (let attack of this.startedAttacks[attackType]) - if (attack.getName() == planName) - return attack; - } - return undefined; -}; - -m.AttackManager.prototype.pausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpausePlan = function(planName) -{ - let attack = this.getPlan(planName); - if (attack) - attack.setPaused(false); -}; - -m.AttackManager.prototype.pauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(true); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(true); -}; - -m.AttackManager.prototype.unpauseAllPlans = function() -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - attack.setPaused(false); - - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - attack.setPaused(false); -}; - -m.AttackManager.prototype.getAttackInPreparation = function(type) -{ - 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. - */ -m.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; - - let veto = {}; - for (let i in this.defeated) - veto[i] = true; - // No rush if enemy too well defended (i.e. iberians) - if (attack.type == "Rush") - { - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - if (!gameState.isPlayerEnemy(i) || veto[i]) - continue; - if (this.defeated[i]) - continue; - let enemyDefense = 0; - for (let ent of gameState.getEnemyStructures(i).values()) - if (ent.hasClass("Tower") || ent.hasClass("Fortress")) - enemyDefense++; - if (enemyDefense > 6) - veto[i] = true; - } - } - - // then if not a huge attack, continue attacking our previous target as long as it has some entities, - // otherwise target the most accessible one - if (attack.type != "HugeAttack") - { - if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && - !this.defeated[this.currentEnemyPlayer] && - gameState.isPlayerEnemy(this.currentEnemyPlayer) && - gameState.getEntities(this.currentEnemyPlayer).hasEntities()) - return this.currentEnemyPlayer; - - let distmin; - let ccmin; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let ourcc of ccEnts.values()) - { - if (ourcc.owner() != PlayerID) - continue; - let ourPos = ourcc.position(); - let access = m.getLandAccess(gameState, ourcc); - for (let enemycc of ccEnts.values()) - { - if (veto[enemycc.owner()]) - continue; - if (!gameState.isPlayerEnemy(enemycc.owner())) - continue; - if (access != m.getLandAccess(gameState, enemycc)) - continue; - let dist = API3.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 (veto[i]) - continue; - if (!gameState.isPlayerEnemy(i)) - continue; - let enemyCount = 0; - let enemyCivCentre = false; - for (let 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; -}; - -/** - * Target the player with the most advanced wonder. - * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. - */ -m.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let enemyWonder; - let moreAdvanced; - for (let wonder of gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")).values()) - { - if (wonder.owner() == 0) - continue; - let progress = wonder.foundationProgress(); - if (progress === undefined) - { - enemyWonder = wonder; - break; - } - if (enemyWonder && moreAdvanced > progress) - continue; - enemyWonder = wonder; - moreAdvanced = progress; - } - if (enemyWonder) - { - enemyPlayer = enemyWonder.owner(); - if (attack.targetPlayer === undefined) - this.currentEnemyPlayer = enemyPlayer; - } - return enemyPlayer; -}; - -/** - * Target the player with the most relics (including gaia). - */ -m.AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) -{ - let enemyPlayer; - let allRelics = gameState.updatingGlobalCollection("allRelics", API3.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; - - let 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. */ -m.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) -{ - for (let attackType in this.upcomingAttacks) - for (let attack of this.upcomingAttacks[attackType]) - if (attack.targetPlayer === player) - attack.targetPlayer = undefined; - - for (let attackType in this.startedAttacks) - for (let i = 0; i < this.startedAttacks[attackType].length; ++i) - { - let attack = this.startedAttacks[attackType][i]; - if (attack.targetPlayer === player) - { - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--, 1); - } - } -}; - -m.AttackManager.prototype.raidTargetEntity = function(gameState, ent) -{ - let data = { "target": ent }; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); - if (attackPlan.failed) - return null; - if (this.Config.debug > 1) - API3.warn("Military Manager: Raiding plan " + this.totalNumber); - this.raidNumber++; - this.totalNumber++; - attackPlan.init(gameState); - this.upcomingAttacks.Raid.push(attackPlan); - return attackPlan; -}; - -/** - * Return the number of units from any of our attacking armies around this position - */ -m.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) -{ - let num = 0; - for (let attackType in this.startedAttacks) - for (let attack of this.startedAttacks[attackType]) - { - if (!attack.position) // this attack may be inside a transport - continue; - if (API3.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 - */ -m.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) -{ - if (!target || !target.position()) - return false; - if (!data.range && !data.armyID) - { - API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); - return false; - } - let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; - let pos = target.position(); - let attackType = "Attack"; - let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); - if (attackPlan.failed) - return false; - this.totalNumber++; - attackPlan.init(gameState); - this.startedAttacks[attackType].push(attackPlan); - - let targetAccess = m.getLandAccess(gameState, target); - for (let army of gameState.ai.HQ.defenseManager.armies) - { - if (data.range) - { - army.recalculatePosition(gameState); - if (API3.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) - { - let unitId = army.ownEntities[0]; - army.removeOwn(gameState, unitId); - let unit = gameState.getEntityById(unitId); - let accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || - unit.position() && m.getLandAccess(gameState, unit) == targetAccess; - if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) - { - unit.setMetadata(PlayerID, "plan", attackPlan.name); - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.unitCollection.updateEnt(unit); - } - } - } - if (!attackPlan.unitCollection.hasEntities()) - { - attackPlan.Abort(gameState); - return false; - } - for (let unit of attackPlan.unitCollection.values()) - unit.setMetadata(PlayerID, "role", "attack"); - attackPlan.targetPlayer = target.owner(); - attackPlan.targetPos = pos; - attackPlan.target = target; - attackPlan.state = "arrived"; - return true; -}; - -m.AttackManager.prototype.Serialize = function() -{ - let properties = { - "totalNumber": this.totalNumber, - "attackNumber": this.attackNumber, - "rushNumber": this.rushNumber, - "raidNumber": this.raidNumber, - "debugTime": this.debugTime, - "maxRushes": this.maxRushes, - "rushSize": this.rushSize, - "currentEnemyPlayer": this.currentEnemyPlayer, - "defeated": this.defeated - }; - - let upcomingAttacks = {}; - for (let key in this.upcomingAttacks) - { - upcomingAttacks[key] = []; - for (let attack of this.upcomingAttacks[key]) - upcomingAttacks[key].push(attack.Serialize()); - } - - let startedAttacks = {}; - for (let key in this.startedAttacks) - { - startedAttacks[key] = []; - for (let attack of this.startedAttacks[key]) - startedAttacks[key].push(attack.Serialize()); - } - - return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; -}; - -m.AttackManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.upcomingAttacks = {}; - for (let key in data.upcomingAttacks) - { - this.upcomingAttacks[key] = []; - for (let dataAttack of data.upcomingAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.upcomingAttacks[key].push(attack); - } - } - - this.startedAttacks = {}; - for (let key in data.startedAttacks) - { - this.startedAttacks[key] = []; - for (let dataAttack of data.startedAttacks[key]) - { - let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); - attack.Deserialize(gameState, dataAttack); - attack.init(gameState); - this.startedAttacks[key].push(attack); - } - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/attackPlan.js b/install/petraBased/petra-unitary/attackPlan.js deleted file mode 100644 index 677fd55..0000000 --- a/install/petraBased/petra-unitary/attackPlan.js +++ /dev/null @@ -1,2176 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This is an attack plan: - * It deals with everything in an attack, from picking a target to picking a path to it - * To making sure units are built, and pushing elements to the queue manager otherwise - * It also handles the actual attack, though much work is needed on that. - */ - -m.AttackPlan = function(gameState, Config, uniqueID, type, data) -{ - this.Config = Config; - this.name = uniqueID; - this.type = type || "Attack"; - this.state = "unexecuted"; - this.forced = false; // true when this attacked has been forced to help an ally - - if (data && data.target) - { - 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.uniqueTargetId = data && data.uniqueTargetId || undefined; - - // get a starting rallyPoint ... will be improved later - let rallyPoint; - let rallyAccess; - let allAccesses = {}; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let access = m.getLandAccess(gameState, base.anchor); - if (!rallyPoint) - { - rallyPoint = base.anchor.position(); - rallyAccess = access; - } - if (!allAccesses[access]) - allAccesses[access] = base.anchor.position(); - } - if (!rallyPoint) // no base ? take the position of any of our entities - { - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position()) - continue; - let access = m.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 (let structure of gameState.getEnemyStructures().values()) - { - if (this.target && structure.id() != this.target.id()) - continue; - if (!structure.position()) - continue; - let access = m.getLandAccess(gameState, structure); - if (access in allAccesses) - { - this.overseas = 0; - this.rallyPoint = allAccesses[access]; - break; - } - else if (!this.overseas) - { - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); - if (!sea) - { - if (this.target) - { - API3.warn("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 = 70; - - // 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 == "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - if (data && data.targetSize) - this.unitStat.Infantry.targetSize = data.targetSize; - this.neededShips = 1; - } - else if (type == "Raid") - { - priority = 150; - this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [ ["strength", 1] ] }; - this.neededShips = 1; - } - else if (type == "HugeAttack") - { - 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.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], - "interests": [["strength", 2]] }; - this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], - "interests": [["strength", 3]] }; - this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "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.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], - "interests": [["strength", 1]] }; - this.neededShips = 3; - } - - // Put some randomness on the attack size - let variation = randFloat(0.8, 1.2); - // and lower priority and smaller sizes for easier difficulty levels - if (this.Config.difficulty < 2) - { - priority *= 0.6; - variation *= 0.5; - } - else if (this.Config.difficulty < 3) - { - priority *= 0.8; - variation *= 0.8; - } - for (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(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 (let cat in this.unitStat) - { - this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); - this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); - } - - // 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 = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders - - // 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; -}; - -m.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(API3.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 (let cat in this.unitStat) - { - let Unit = this.unitStat[cat]; - this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[cat].registerUpdates(); - if (this.canBuildUnits) - this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); - } -}; - -m.AttackPlan.prototype.getName = function() -{ - return this.name; -}; - -m.AttackPlan.prototype.getType = function() -{ - return this.type; -}; - -m.AttackPlan.prototype.isStarted = function() -{ - return this.state !== "unexecuted" && this.state !== "completing"; -}; - -m.AttackPlan.prototype.isPaused = function() -{ - return this.paused; -}; - -m.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. - */ -m.AttackPlan.prototype.canStart = function() -{ - if (!this.canBuildUnits) - return true; - - for (let unitCat in this.unitStat) - if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) - return false; - - return true; -}; - -m.AttackPlan.prototype.mustStart = function() -{ - if (this.isPaused()) - return false; - - if (!this.canBuildUnits) - return this.unitCollection.hasEntities(); - - let MaxReachedEverywhere = true; - let MinReachedEverywhere = true; - for (let unitCat in this.unitStat) - { - let 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 == "Raid" && this.target && this.target.foundationProgress() && - this.target.foundationProgress() > 50; - return false; -}; - -m.AttackPlan.prototype.forceStart = function() -{ - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - Unit.targetSize = 0; - Unit.minSize = 0; - } - this.forced = true; -}; - -/** Adds a build order. If resetQueue is true, this will reset the queue. */ -m.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; - let Unit = this.unitStat[name]; - this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); - this.unit[name].registerUpdates(); - this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); - if (resetQueue) - { - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - } -}; - -m.AttackPlan.prototype.addSiegeUnits = function(gameState) -{ - if (this.siegeState == 2 || this.state !== "unexecuted") - return false; - - let civ = gameState.getPlayerCiv(); - let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; - let hasTrainer = [false, false, false]; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - let trainables = ent.trainableEntities(civ); - if (!trainables) - continue; - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.available(gameState)) - continue; - for (let i = 0; i < classes.length; ++i) - if (classes[i].every(c => template.hasClass(c))) - hasTrainer[i] = true; - } - } - if (hasTrainer.every(e => !e)) - return false; - let i = this.name % classes.length; - for (let k = 0; k < classes.length; ++k) - { - if (hasTrainer[i]) - break; - i = ++i % classes.length; - } - - this.siegeState = 2; - let targetSize; - if (this.Config.difficulty < 3) - targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); - else - targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; - targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); - if (!targetSize) - return true; - // no minsize as we don't want the plan to fail at the last minute though. - let 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". */ -m.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 == "completing") - { - // if our target was destroyed, go back to "unexecuted" state - if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) - { - this.state = "unexecuted"; - this.target = undefined; - } - else - { - // check that all units have finished with their transport if needed - if (this.waitingForTransport()) - return 1; - // bloqued units which cannot finish their order should not stop the attack - if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) - return 1; - return 2; - } - } - - 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 1; - - if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) - this.assignUnits(gameState); - if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) - this.reassignCavUnit(gameState); // reassign some cav (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.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - } - else // Abort the plan so that its units will be reassigned to other plans. - { - if (this.Config.debug > 1) - { - let am = gameState.ai.HQ.attackManager; - API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + - " rush " + am.upcomingAttacks.Rush.length + - " attack " + am.upcomingAttacks.Attack.length + - " huge " + am.upcomingAttacks.HugeAttack.length); - API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + - " rush " + am.startedAttacks.Rush.length + - " attack " + am.startedAttacks.Attack.length + - " huge " + am.startedAttacks.HugeAttack.length); - } - return 0; - } - } - else if (this.mustStart()) - { - if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) - { - // keep on while the units finish being trained, then we'll start - this.queue.empty(); - this.queueChamp.empty(); - this.queueSiege.empty(); - return 1; - } - } - else - { - if (this.canBuildUnits) - { - // We still have time left to recruit units and do stuffs. - if (this.siegeState == 0 || this.siegeState == 1 && 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 0; // will abort the plan - } - return 1; - } - - // if we're here, it means we must start - this.state = "completing"; - - // Raids have their predefined target - if (!this.target && !this.chooseTarget(gameState)) - return 0; - if (!this.overseas) - this.getPathToTarget(gameState); - - if (this.type == "Raid") - this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; - else - { - if (this.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 (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) - { - for (let entId of unitIds.values()) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) - continue; - unitIds.delete(entId); - ent.stopMoving(); - } - } - - let rallyPoint = this.rallyPoint; - let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); - for (let 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", "attack"); - ent.setMetadata(PlayerID, "subrole", "completing"); - let queued = false; - if (ent.resourceCarrying() && ent.resourceCarrying().length) - queued = m.returnResources(gameState, ent); - let index = m.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 - let plan = this.name; - gameState.ai.queueManager.removeQueue("plan_" + plan); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); - return 1; -}; - - -m.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 (let order of this.buildOrders) - { - let 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; - return va - vb; - }); - - if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) - { - API3.warn("===================================="); - API3.warn("======== build order for plan " + this.name); - for (let order of this.buildOrders) - { - let specialData = "Plan_"+this.name+"_"+order[4]; - let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); - let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); - let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); - let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); - API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + - " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); - } - API3.warn("===================================="); - } - - let firstOrder = this.buildOrders[0]; - if (firstOrder[0] < firstOrder[3].targetSize) - { - // find the actual queue we want - let queue = this.queue; - if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && - firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) - 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) - { - let 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) - API3.warn("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) - API3.warn("attack template " + template + " added for plan " + this.name); - let max = firstOrder[3].batchSize; - let specialData = "Plan_" + this.name + "_" + firstOrder[4]; - let data = { "plan": this.name, "special": specialData, "base": 0 }; - data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; - let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); - if (trainingPlan.template) - queue.addPlan(trainingPlan); - else if (this.Config.debug > 1) - API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + - " build3 " + uneval(firstOrder[3].interests)); - } - } - } -}; - -m.AttackPlan.prototype.assignUnits = function(gameState) -{ - let 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 (let 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 == "Raid") - { - // Raid are fast cavalry attack: assign all cav except some for hunting - let num = 0; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Cavalry") || !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 (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) - { - if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) - continue; - if (ent.hasClass("Ship") || ent.hasClass("Support") || 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 (let 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, - // 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 - let num = 0; - let numbase = {}; - let keep = this.type != "Rush" ? - 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; - keep = Math.round(this.Config.popScaling * keep); - for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) - { - if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) - continue; - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID) - numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; - else - { - API3.warn("Petra problem ent without base "); - m.dumpEntity(ent); - continue; - } - if (num++ < keep || numbase[baseID] < 5) - continue; - if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") - continue; - ent.setMetadata(PlayerID, "plan", plan); - this.unitCollection.updateEnt(ent); - added = true; - } - return added; -}; - -m.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) Cav unit to fasten raid preparation. */ -m.AttackPlan.prototype.reassignCavUnit = function(gameState) -{ - for (let ent of this.unitCollection.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) - continue; - let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); - ent.setMetadata(PlayerID, "plan", raid.name); - this.unitCollection.updateEnt(ent); - raid.unitCollection.updateEnt(ent); - return; - } -}; - -m.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) - let targetIndex = m.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 (let base of gameState.ai.HQ.baseManagers) - { - let anchor = base.anchor; - if (!anchor || !anchor.position()) - continue; - let dist = API3.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; - let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); - if (sea) - { - this.overseas = sea; - gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); - } - else - { - API3.warn("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 - */ -m.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 API3.EntityCollection(gameState.sharedScript); - let ent = gameState.getEntityById(this.uniqueTargetId); - if (ent) - targets.addEnt(ent); - } - else - { - if (this.type == "Raid") - targets = this.raidTargetFinder(gameState); - else if (this.type == "Rush" || this.type == "Attack") - { - 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 (let 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 = API3.SquareVectorDistance(ent.position(), position); - // In normal attacks, disfavor fields - if (this.type != "Rush" && this.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 - */ -m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - if (gameState.getVictoryConditions().has("wonder")) - for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("regicide")) - for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) - targets.addEnt(ent); - if (gameState.getVictoryConditions().has("capture_the_relic")) - for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) - targets.addEnt(ent); - targets = targets.filter(this.isValidTarget, this); - if (targets.hasEntities()) - return targets; - - let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); - targets = validTargets.filter(API3.Filters.byClass("CivCentre")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); - // If there's nothing, attack anything else that's less critical - if (!targets.hasEntities()) - targets = validTargets.filter(API3.Filters.byClass("Town")); - if (!targets.hasEntities()) - targets = validTargets.filter(API3.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(API3.Filters.byClass("ConquestCritical")). - filter(API3.Filters.not(API3.Filters.byClass("Ship"))); - return targets; -}; - -m.AttackPlan.prototype.isValidTarget = function(ent) -{ - if (!ent.position()) - return false; - if (this.sameLand && m.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 */ -m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) -{ - let targets = new API3.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 (let building of buildings) - { - if (building.owner() == 0) - continue; - if (building.hasDefensiveFire()) - continue; - if (!this.isValidTarget(building)) - continue; - let pos = building.position(); - let defended = false; - for (let defense of buildings) - { - if (!defense.hasDefensiveFire()) - continue; - let dist = API3.SquareVectorDistance(pos, defense.position()); - if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 - { - defended = true; - break; - } - } - if (defended) - continue; - let dist = API3.SquareVectorDistance(pos, this.position); - if (dist > minDist) - continue; - minDist = dist; - target = building; - } - if (target) - targets.addEnt(target); - - if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) - targets = this.rushTargetFinder(gameState); - - return targets; -}; - -/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ -m.AttackPlan.prototype.raidTargetFinder = function(gameState) -{ - let targets = new API3.EntityCollection(gameState.sharedScript); - for (let targetId of gameState.ai.HQ.defenseManager.targetList) - { - let 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 - */ -m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) -{ - if (m.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) - return target; - - let targetPos = target.position(); - let startPos = { "x": position[0], "y": position[1] }; - let endPos = { "x": targetPos[0], "y": targetPos[1] }; - let blocker; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); - if (!path.length) - return undefined; - - let pathPos = [path[0].x, path[0].y]; - let dist = API3.VectorDistance(pathPos, targetPos); - let radius = target.obstructionRadius().max; - for (let 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("Gates")) - 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. - let structPos = struct.position(); - let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; - let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; - - if (struct.get("Obstruction/Static")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Static/@width"); - let depth = +struct.get("Obstruction/Static/@depth"); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - } - else if (struct.get("Obstruction/Obstructions")) - { - if (!struct.angle()) - continue; - let angle = struct.angle(); - let width = +struct.get("Obstruction/Obstructions/Door/@width"); - let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); - let 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")); - let cosa = Math.cos(angle); - let sina = Math.sin(angle); - let u = x * cosa - y * sina; - let v = x * sina + y * cosa; - if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) - { - blocker = struct; - break; - } - // check that the path does not cross this gate (could happen if not locked) - for (let i = 1; i < path.length; ++i) - { - let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; - let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; - let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; - let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; - if (v1 * v2 < 0) - { - let 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")) - { - let r = +this.get("Obstruction/Unit/@radius"); - if (x*x + y*y < r*r) - { - blocker = struct; - break; - } - } - } - - if (blocker && blocker.hasClass("StoneWall")) - { -/* if (this.hasSiegeUnits()) - { */ - this.isBlocked = true; - return blocker; -/* } - return undefined; */ - } - else if (blocker) - { - this.isBlocked = true; - return blocker; - } - - return target; -}; - -m.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) -{ - let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let endAccess = m.getLandAccess(gameState, this.target); - if (startAccess != endAccess) - return false; - - Engine.ProfileStart("AI Compute path"); - let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; - let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; - let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); - this.path = []; - this.path.push(this.targetPos); - for (let p in path) - this.path.push([path[p].x, path[p].y]); - this.path.push(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 */ -m.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. - */ -m.AttackPlan.prototype.StartAttack = function(gameState) -{ - - if (this.Config.debug > 1) - API3.warn("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. - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); - - // Wait to attack! - if( gameState.getPopulation() < 0.85*gameState.getPopulationMax() ) - return false; - - for (let ent of this.unitCollection.values()) - { - ent.setMetadata(PlayerID, "subrole", "walking"); - let stance = ent.isPackable() ? "standground" : "aggressive"; - if (ent.getStance() != stance) - ent.setStance(stance); - } - - let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); - let targetAccess = m.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 (let 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 */ -m.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.state == "walking" && !this.UpdateWalking(gameState, events)) - { - Engine.ProfileStop(); - return 0; - } - - if (this.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", "attacking"); - }); - if (this.type == "Rush") // try to find a better target for rush - { - let 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; - } - - let time = gameState.ai.elapsedTime; - let attackedByStructure = {}; - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - let ourUnit = gameState.getEntityById(evt.target); - if (!ourUnit || !attacker || !attacker.position()) - continue; - if (!attacker.hasClass("Unit")) - { - attackedByStructure[evt.target] = true; - continue; - } - if (m.isSiegeUnit(ourUnit)) - { // if our siege units are attacked, we'll send some units to deal with enemies. - let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - // And if this attacker is a non-ranged siege unit and our unit also, attack it - if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) - { - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) - { - // 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 (m.isSiegeUnit(attacker)) - { // if our unit is attacked by a siege unit, we'll send some melee units to help it. - let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); - for (let ent of collec.values()) - { - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - else - { - // Look first for nearby units to help us if possible - let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); - for (let ent of collec.values()) - { - if (m.isSiegeUnit(ent)) - continue; - let orderData = ent.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - continue; - } - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - 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 - let orderData = ourUnit.unitAIOrderData(); - if (orderData && orderData.length && orderData[0].target) - { - if (orderData[0].target === attacker.id()) - continue; - let target = gameState.getEntityById(orderData[0].target); - if (target && !target.hasClass("Structure") && !target.hasClass("Support")) - { - if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) - continue; - } - } - ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - } - } - } - - let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); - let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); - - // Count the number of times an enemy is targeted, to prevent all units to follow the same target - let unitTargets = {}; - for (let ent of this.unitCollection.values()) - { - if (ent.hasClass("Ship")) // TODO What to do with ships - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || target.hasClass("Structure")) - continue; - if (!(targetId in unitTargets)) - { - if (m.isSiegeUnit(target) || target.hasClass("Hero")) - unitTargets[targetId] = -8; - else if (target.hasClass("Champion") || target.hasClass("Ship")) - unitTargets[targetId] = -5; - else - unitTargets[targetId] = -3; - } - ++unitTargets[targetId]; - } - let veto = {}; - for (let target in unitTargets) - if (unitTargets[target] > 0) - veto[target] = true; - - let targetClassesUnit; - let targetClassesSiege; - if (this.type == "Rush") - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto }; - else - { - if (this.target.hasClass("Fortress")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto }; - else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; - else - targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "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", "Blacksmith"); - targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); - } - - 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 - let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; - for (let check = 0; check < lgth; check++) - { - let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); - if (!ent || !ent.position()) - continue; - // Do not reaffect units which have reacted to an attack in that same turn - if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) - continue; - - let targetId; - let 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; - let siegeUnit = m.isSiegeUnit(ent); - if (ent.isIdle()) - needsUpdate = true; - else if (siegeUnit && targetId) - { - let 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) - { - let 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("Cavalry") && !ent.hasClass("Ranged") && - target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") - maybeUpdate = true; - } - - // don't update too soon if not necessary - if (!needsUpdate) - { - if (!maybeUpdate) - continue; - let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; - let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); - if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) - continue; - } - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); - let range = 60; - let 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("Cavalry")) - range += 30; - range = range * range; - let entAccess = m.getLandAccess(gameState, ent); - // Checking for gates if we're a siege unit. - if (siegeUnit) - { - let mStruct = enemyStructures.filter(enemy => { - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (enemy.foundationProgress() == 0) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasDefensiveFire()) - vala += 1000; - else if (structa.hasClass("ConquestCritical")) - vala += 200; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasDefensiveFire()) - valb += 1000; - else if (structb.hasClass("ConquestCritical")) - valb += 200; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); - } - } - else - { - if (!ent.hasClass("Ranged")) - { - let 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 - { - let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); - let mUnit = enemyUnits.filter(enemy => { - if (!enemy.position()) - return false; - if (enemy.hasClass("Animal")) - return false; - if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") - return false; - let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); - if (dist > range) - return false; - if (m.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()] && API3.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.countersClasses(unitA.classes())) - vala += 100; - let valb = unitB.hasClass("Support") ? 50 : 0; - if (ent.countersClasses(unitB.classes())) - valb += 100; - let distA = unitA.getMetadata(PlayerID, "distance"); - let 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; - }); - let rand = randIntExclusive(0, mUnit.length * 0.1); - ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); - } - else if (this.isBlocked) - ent.attack(this.target.id(), false); - else if (API3.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.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; - else - targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; - } - else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) - targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; - ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); - } - else - { - let mStruct = enemyStructures.filter(enemy => { - if (this.isBlocked && enemy.id() != this.target.id()) - return false; - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) - return false; - if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) - return false; - if (m.getLandAccess(gameState, enemy) != entAccess) - return false; - return true; - }, this).toEntityArray(); - if (mStruct.length) - { - mStruct.sort((structa, structb) => { - let vala = structa.costSum(); - if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) - vala += 10000; - else if (structa.hasClass("ConquestCritical")) - vala += 100; - let valb = structb.costSum(); - if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) - valb += 10000; - else if (structb.hasClass("ConquestCritical")) - valb += 100; - return valb - vala; - }); - if (mStruct[0].hasClass("Gates")) - ent.attack(mStruct[0].id(), false); - else - { - let rand = randIntExclusive(0, mStruct.length * 0.2); - ent.attack(mStruct[rand].id(), m.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; - if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) - return; - let dist = API3.SquareVectorDistance(unit.position(), ent.position()); - if (dist > distmin) - return; - distmin = dist; - attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); - }); - if (attacker) - ent.attack(attacker.id(), m.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.lastPosition = this.position; - Engine.ProfileStop(); - - return this.unitCollection.length; -}; - -m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) -{ - let done = true; - for (let 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; - } - - if (done) - { - this.state = "arrived"; - return; - } - - // if we are attacked while waiting the rest of the army, retaliate - for (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let attacker = gameState.getEntityById(evt.attacker); - if (!attacker || !gameState.getEntityById(evt.target)) - continue; - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "transport") !== undefined) - continue; - if (!ent.isIdle()) - continue; - ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); - } - break; - } -}; - -m.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 (let evt of events.Attacked) - { - if (!this.unitCollection.hasEntId(evt.target)) - continue; - let 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 = "arrived"; - return true; - } - } - - // basically haven't moved an inch: very likely stuck) - if (API3.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 (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) - { - let dist = API3.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 = this.position; - - if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) - { - if (!this.path[0][0] || !this.path[0][1]) - API3.warn("Start: Problem with path " + uneval(this.path)); - // We're stuck, presumably. Check if there are no walls just close to us. - for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) - { - if (API3.SquareVectorDistance(this.position, ent.position()) > 800) - continue; - let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; - // there are walls, so check if we can attack - if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); - this.state = "arrived"; - return true; - } - // abort plan - if (this.Config.debug > 1) - API3.warn("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 (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) - { - 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) - API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); - this.state = "arrived"; - return true; - } - } - - return true; -}; - -m.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) - { - let holder = m.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) - API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); - let accessIndex = this.getAttackAccess(gameState); - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.uniqueTargetId) - return false; - - // Check if we could help any current attack - let attackManager = gameState.ai.HQ.attackManager; - for (let attackType in attackManager.startedAttacks) - { - for (let 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 != m.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) - { - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - if (this.targetPlayer !== undefined) - this.target = this.getNearestTarget(gameState, this.position, accessIndex); - if (!this.target) - { - if (this.Config.debug > 1) - API3.warn("No new target found. Remaining units " + this.unitCollection.length); - return false; - } - } - if (this.Config.debug > 1) - API3.warn("We will help one of our other attacks"); - } - this.targetPos = this.target.position(); - } - return true; -}; - -/** reset any units */ -m.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()) - { - let access = this.getAttackAccess(gameState); - let dist = Math.min(); - if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) - { - rallyPoint = this.rallyPoint; - dist = API3.SquareVectorDistance(this.position, rallyPoint); - } - // Then check if we have a nearer base (in case this attack has captured one) - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); - if (newdist > dist) - continue; - dist = newdist; - rallyPoint = base.anchor.position(); - } - } - - for (let ent of this.unitCollection.values()) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - ent.stopMoving(); - if (rallyPoint) - ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); - this.removeUnit(ent); - } - } - - for (let unitCat in this.unitStat) - this.unit[unitCat].unregister(); - - gameState.ai.queueManager.removeQueue("plan_" + this.name); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); - gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); -}; - -m.AttackPlan.prototype.removeUnit = function(ent, update) -{ - if (ent.getMetadata(PlayerID, "role") == "attack") - { - if (ent.hasClass("CitizenSoldier")) - ent.setMetadata(PlayerID, "role", "worker"); - else - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - } - ent.setMetadata(PlayerID, "plan", -1); - if (update) - this.unitCollection.updateEnt(ent); -}; - -m.AttackPlan.prototype.checkEvents = function(gameState, events) -{ - for (let evt of events.EntityRenamed) - { - if (!this.target || this.target.id() != evt.entity) - continue; - if (this.type == "Raid" && !this.isStarted()) - this.target = undefined; - else - this.target = gameState.getEntityById(evt.newentity); - if (this.target) - this.targetPos = this.target.position(); - } - - for (let evt of events.OwnershipChanged) // capture event - if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) - this.target = undefined; - - for (let evt of events.PlayerDefeated) - { - if (this.targetPlayer !== evt.playerId) - continue; - this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); - this.target = undefined; - } - - if (!this.overseas || this.state !== "unexecuted") - return; - // let's check if an enemy has built a structure at our access - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.position() || !ent.hasClass("Structure")) - continue; - if (!gameState.isPlayerEnemy(ent.owner())) - continue; - let access = m.getLandAccess(gameState, ent); - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - this.overseas = 0; - this.rallyPoint = base.anchor.position(); - } - } -}; - -m.AttackPlan.prototype.waitingForTransport = function() -{ - for (let ent of this.unitCollection.values()) - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return true; - return false; -}; - -m.AttackPlan.prototype.hasSiegeUnits = function() -{ - for (let ent of this.unitCollection.values()) - if (m.isSiegeUnit(ent)) - return true; - return false; -}; - -m.AttackPlan.prototype.hasForceOrder = function(data, value) -{ - for (let ent of this.unitCollection.values()) - { - if (data && +ent.getMetadata(PlayerID, data) !== value) - continue; - let orders = ent.unitAIOrderData(); - for (let order of orders) - if (order.force) - return true; - } - 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. - */ -m.AttackPlan.prototype.getAttackAccess = function(gameState) -{ - for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) - return m.getLandAccess(gameState, ent); - - return 0; -}; - -m.AttackPlan.prototype.debugAttack = function() -{ - API3.warn("---------- attack " + this.name); - for (let unitCat in this.unitStat) - { - let Unit = this.unitStat[unitCat]; - API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); - } - API3.warn("------------------------------"); -}; - -m.AttackPlan.prototype.Serialize = function() -{ - let properties = { - "name": this.name, - "type": this.type, - "state": this.state, - "forced": this.forced, - "rallyPoint": 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, - "isBlocked": this.isBlocked, - "targetPlayer": this.targetPlayer, - "target": this.target !== undefined ? this.target.id() : undefined, - "targetPos": this.targetPos, - "uniqueTargetId": this.uniqueTargetId, - "path": this.path - }; - - return { "properties": properties }; -}; - -m.AttackPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - if (this.target) - this.target = gameState.getEntityById(this.target); - - this.failed = undefined; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/baseManager.js b/install/petraBased/petra-unitary/baseManager.js deleted file mode 100644 index b2260f6..0000000 --- a/install/petraBased/petra-unitary/baseManager.js +++ /dev/null @@ -1,1111 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Base Manager - * Handles lower level economic stuffs. - * Some tasks: - * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. - * -giving feedback/estimates on GR - * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. - * -getting good spots for dropsites - * -managing dropsite use in the base - * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) - */ - -m.BaseManager = function(gameState, Config) -{ - this.Config = Config; - this.ID = gameState.ai.uniqueIDs.bases++; - - // anchor building: seen as the main building of the base. Needs to have territorial influence - this.anchor = undefined; - this.anchorId = undefined; - this.accessIndex = undefined; - - // Maximum distance (from any dropsite) to look for resources - // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max - this.maxDistResourceSquare = 360*360; - - this.constructing = false; - // Defenders to train in this cc when its construction is finished - this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; - - // vector for iterating, to check one use the HQ map. - this.territoryIndices = []; - - this.timeNextIdleCheck = 0; -}; - -m.BaseManager.prototype.init = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else if (state != "captured") - this.neededDefenders = 0; - this.workerObject = new m.Worker(this); - // entitycollections - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); - this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); - this.mobileDropsites = this.units.filter(API3.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 (let res of Resources.GetCodes()) - { - this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; - this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; - } -}; - -m.BaseManager.prototype.reset = function(gameState, state) -{ - if (state == "unconstructed") - this.constructing = true; - else - this.constructing = false; - - if (state != "captured" || this.Config.difficulty < 3) - this.neededDefenders = 0; - else - this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); -}; - -m.BaseManager.prototype.assignEntity = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "base", this.ID); - this.units.updateEnt(ent); - this.workers.updateEnt(ent); - this.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) - this.assignResourceToDropsite(gameState, ent); -}; - -m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) -{ - if (!anchorEntity.hasClass("CivCentre")) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); - else - { - this.anchor = anchorEntity; - this.anchorId = anchorEntity.id(); - this.anchor.setMetadata(PlayerID, "baseAnchor", true); - gameState.ai.HQ.resetBaseCache(); - } - anchorEntity.setMetadata(PlayerID, "base", this.ID); - this.buildings.updateEnt(anchorEntity); - this.accessIndex = m.getLandAccess(gameState, anchorEntity); - return true; -}; - -/* we lost our anchor. Let's reaffect our units and buildings */ -m.BaseManager.prototype.anchorLost = function(gameState, ent) -{ - this.anchor = undefined; - this.anchorId = undefined; - this.neededDefenders = 0; - gameState.ai.HQ.resetBaseCache(); -}; - -/** Set a building of an anchorless base */ -m.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) -{ - if (!this.buildings.hasEntities()) - { - if (!m.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) - API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); - this.accessIndex = m.getLandAccess(gameState, ent); - } - else if (this.accessIndex != m.getLandAccess(gameState, ent)) - API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + - " has been assigned " + ent.templateName() + " with access" + m.getLandAccess(gameState, ent)); - - 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. - */ -m.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) -{ - if (this.dropsites[dropsite.id()]) - { - if (this.Config.debug > 0) - warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); - return; - } - - let accessIndex = this.accessIndex; - let dropsitePos = dropsite.position(); - let dropsiteId = dropsite.id(); - this.dropsites[dropsiteId] = true; - - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) - accessIndex = m.getLandAccess(gameState, dropsite); - - let maxDistResourceSquare = this.maxDistResourceSquare; - for (let type of dropsite.resourceDropsiteTypes()) - { - let resources = gameState.getResourceSupplies(type); - if (!resources.length) - continue; - - let nearby = this.dropsiteSupplies[type].nearby; - let medium = this.dropsiteSupplies[type].medium; - let faraway = this.dropsiteSupplies[type].faraway; - - resources.forEach(function(supply) - { - if (!supply.position()) - return; - if (supply.hasClass("Animal")) // moving resources are treated differently - return; - if (supply.hasClass("Field")) // fields are treated separately - return; - if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately - return; - // quick accessibility check - if (m.getLandAccess(gameState, supply) != accessIndex) - return; - - let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); - if (dist < maxDistResourceSquare) - { - if (dist < maxDistResourceSquare/16) // distmax/4 - nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else if (dist < maxDistResourceSquare/4) // distmax/2 - medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - else - faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); - } - }); - - nearby.sort((r1, r2) => r1.dist - r2.dist); - medium.sort((r1, r2) => r1.dist - r2.dist); - faraway.sort((r1, r2) => r1.dist - r2.dist); - -/* let debug = false; - if (debug) - { - faraway.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); - }); - medium.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); - }); - nearby.forEach(function(res){ - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); - }); - } */ - } - - // Allows all allies to use this dropsite except if base anchor to be sure to keep - // a minimum of resources for this base - Engine.PostCommand(PlayerID, { - "type": "set-dropsite-sharing", - "entities": [dropsiteId], - "shared": dropsiteId != this.anchorId - }); -}; - -// completely remove the dropsite resources from our list. -m.BaseManager.prototype.removeDropsite = function(gameState, ent) -{ - if (!ent.id()) - return; - - let removeSupply = function(entId, supply){ - for (let i = 0; i < supply.length; ++i) - { - // exhausted resource, remove it from this list - if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) - supply.splice(i--, 1); - // resource assigned to the removed dropsite, remove it - else if (supply[i].dropsite == entId) - supply.splice(i--, 1); - } - }; - - for (let type in this.dropsiteSupplies) - { - removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); - removeSupply(ent.id(), this.dropsiteSupplies[type].medium); - removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); - } - - this.dropsites[ent.id()] = undefined; -}; - -/** - * Returns the position of the best place to build a new dropsite for the specified resource - */ -m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) -{ - - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then checks for a good spot in the territory. If none, and town/city phase, checks outside - // The AI will currently not build a CC if it wouldn't connect with an existing CC. - - let obstructions = m.createObstructionMap(gameState, this.accessIndex, template); - - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray(); - - let bestIdx; - let bestVal = 0; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let cellSize = territoryMap.cellSize; - - for (let j of this.territoryIndices) - { - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) // no room around - continue; - - // we add 3 times the needed resource and once the others (except food) - let total = 2*gameState.sharedScript.resourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - total += gameState.sharedScript.resourceMaps[res].map[j]; - - total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 - if (total <= bestVal) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - for (let dp of dpEnts) - { - let dpPos = dp.position(); - if (!dpPos) - continue; - let dist = API3.SquareVectorDistance(dpPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - - for (let cc of ccEnts) - { - let ccPos = cc.position(); - if (!ccPos) - continue; - let dist = API3.SquareVectorDistance(ccPos, pos); - if (dist < 3600) - { - total = 0; - break; - } - else if (dist < 6400) - total *= (Math.sqrt(dist)-60)/20; - } - if (total <= bestVal) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = total; - bestIdx = i; - } - - if (this.Config.debug > 2) - warn(" for dropsite best is " + bestVal); - - if (bestVal <= 0) - return { "quality": bestVal, "pos": [0, 0] }; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return { "quality": bestVal, "pos": [x, z] }; -}; - -m.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) -{ - let count = 0; - let check = {}; - for (let supply of this.dropsiteSupplies[type].nearby) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - count += supply.ent.resourceSupplyAmount(); - } - if (nearbyOnly) - return count; - - for (let supply of this.dropsiteSupplies[type].medium) - { - if (check[supply.id]) - continue; - check[supply.id] = true; - count += 0.6*supply.ent.resourceSupplyAmount(); - } - return count; -}; - -/** check our resource levels and react accordingly */ -m.BaseManager.prototype.checkResourceLevels = function(gameState, queues) -{ - for (let type of Resources.GetCodes()) - { - if (type == "food") - { - if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations - let numQueue = queues.field.countQueuedUnits(); - - // TODO if not yet farms, add a check on time used/lost and build farmstead if needed - if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them - { - if (count < 600) - { - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID })); - gameState.ai.HQ.needFarm = true; - } - } - else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) - { - let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; - let goal = this.Config.Economy.provisionFields; - if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) - goal = Math.max(goal-1, 1); - if (numFound + numQueue < goal) - queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "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 m.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")) - { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted - if (count < 900) - { - queues.corral.addPlan(new m.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(API3.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 (let ent of this.gatherersByType(gameState, type).values()) - { - if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") - ++this.gatherers[type].used; - else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - ++this.gatherers[type].lost; - } - // TODO add also a test on remaining resources. - let total = this.gatherers[type].used + this.gatherers[type].lost; - if (total > 150 || total > 60 && type != "wood") - { - let ratio = this.gatherers[type].lost / total; - if (ratio > 0.15) - { - let newDP = this.findBestDropsiteLocation(gameState, type); - if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) - { - // No good dropsite, try to build a new base if no base already planned, - // and if not possible, be less strict on dropsite quality. - if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && - newDP.quality > Math.min(25, 50*0.15/ratio) && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); - } - } - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; - this.gatherers[type].used = 0; - this.gatherers[type].lost = 0; - } - else if (total == 0) - this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; - } - -}; - -/** Adds the estimated gather rates from this base to the currentRates */ -m.BaseManager.prototype.addGatherRates = function(gameState, currentRates) -{ - for (let res in currentRates) - { - // I calculate the exact gathering rate for each unit. - // I must then lower that to account for travel time. - // Given that the faster you gather, the more travel time matters, - // I use some logarithms. - // TODO: this should take into account for unit speed and/or distance to target - - this.gatherersByType(gameState, res).forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - if (res == "food") - { - this.workersBySubrole(gameState, "hunter").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - this.workersBySubrole(gameState, "fisher").forEach(ent => { - if (ent.isIdle() || !ent.position()) - return; - let gRate = ent.currentGatherRate(); - if (gRate) - currentRates[res] += Math.log(1+gRate)/1.1; - }); - } - } -}; - -m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) -{ - if (!roleless) - roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); - - for (let ent of roleless) - { - if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "role", "worker"); - else if (ent.hasClass("Support") && ent.hasClass("Elephant")) - ent.setMetadata(PlayerID, "role", "worker"); - } -}; - -/** - * If the numbers of workers on the resources is unbalanced then set some of workers to idle so - * they can be reassigned by reassignIdleWorkers. - * TODO: actually this probably should be in the HQ. - */ -m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) -{ - this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; - // change resource only towards one which is more needed, and if changing will not change this order - let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let sumWanted = 0; - let sumCurrent = 0; - for (let need of mostNeeded) - { - sumWanted += need.wanted; - sumCurrent += need.current; - } - let scale = 1; - if (sumWanted > 0) - scale = sumCurrent / sumWanted; - - for (let i = mostNeeded.length-1; i > 0; --i) - { - let lessNeed = mostNeeded[i]; - for (let j = 0; j < i; ++j) - { - let moreNeed = mostNeeded[j]; - let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) - { - if (lessNeed.type != "food" && gameState.ai.HQ.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" && gameState.ai.HQ.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 FemaleCitizen for food and CitizenSoldier for other resources. - */ -m.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) -{ - let num = number; - let only; - let gatherers = this.gatherersByType(gameState, from); - if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) - only = "CitizenSoldier"; - else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) - only = "FemaleCitizen"; - - for (let 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); - gameState.ai.HQ.AddTCResGatherer(to); - } - return num; -}; - -m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) -{ - // Search for idle workers, and tell them to gather resources based on demand - if (!idleWorkers) - { - let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); - idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); - } - - for (let ent of idleWorkers) - { - // Check that the worker isn't garrisoned - if (!ent.position()) - continue; - // Support elephant can only be builders - if (ent.hasClass("Support") && ent.hasClass("Elephant")) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - continue; - } - - if (ent.hasClass("Worker")) - { - // Just emergency repairing here. It is better managed in assignToFoundations - if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && - gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) - ent.repair(this.anchor); - else if (ent.isGatherer()) - { - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - for (let needed of mostNeeded) - { - if (!ent.canGather(needed.type)) - continue; - let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; - if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) - continue; - if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) - continue; - ent.setMetadata(PlayerID, "subrole", "gatherer"); - ent.setMetadata(PlayerID, "gather-type", needed.type); - gameState.ai.HQ.AddTCResGatherer(needed.type); - break; - } - } - } - else if (ent.hasClass("Cavalry")) - ent.setMetadata(PlayerID, "subrole", "hunter"); - else if (ent.hasClass("FishingBoat")) - ent.setMetadata(PlayerID, "subrole", "fisher"); - } -}; - -m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) -{ - return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); -}; - -m.BaseManager.prototype.gatherersByType = function(gameState, type) -{ - return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); -}; - -/** - * returns an entity collection of workers. - * They are idled immediatly and their subrole set to idle. - */ -m.BaseManager.prototype.pickBuilders = function(gameState, workers, number) -{ - let availableWorkers = this.workers.filter(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") == "builder") - vala = 100; - if (b.getMetadata(PlayerID, "subrole") == "builder") - valb = 100; - if (a.getMetadata(PlayerID, "subrole") == "idle") - vala = -50; - if (b.getMetadata(PlayerID, "subrole") == "idle") - valb = -50; - if (a.getMetadata(PlayerID, "plan") === undefined) - vala = -20; - if (b.getMetadata(PlayerID, "plan") === undefined) - valb = -20; - return vala - valb; - }); - let needed = Math.min(number, availableWorkers.length - 3); - for (let i = 0; i < needed; ++i) - { - availableWorkers[i].stopMoving(); - availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); - workers.addEnt(availableWorkers[i]); - } - return; -}; - -/** - * If we have some foundations, and we don't have enough builder-workers, - * try reassigning some other workers who are nearby - * AI tries to use builders sensibly, not completely stopping its econ. - */ -m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) -{ - let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); - - let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); - - // Check if nothing to build - if (!foundations.length && !damagedBuildings.length) - return; - - let workers = this.workers.filter(ent => ent.isBuilder()); - let builderWorkers = this.workersBySubrole(gameState, "builder"); - let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); - - // if we're constructing and we have the foundations to our base anchor, only try building that. - if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) - { - foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); - let tID = foundations.toEntityArray()[0].id(); - workers.forEach(ent => { - let target = ent.getMetadata(PlayerID, "target-foundation"); - if (target && target != tID) - { - ent.stopMoving(); - ent.setMetadata(PlayerID, "target-foundation", tID); - } - }); - } - - if (workers.length < 3) - { - let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); - if (fromOtherBase) - { - let baseID = this.ID; - fromOtherBase.forEach(worker => { - worker.setMetadata(PlayerID, "base", baseID); - worker.setMetadata(PlayerID, "subrole", "builder"); - workers.updateEnt(worker); - builderWorkers.updateEnt(worker); - idleBuilderWorkers.updateEnt(worker); - }); - } - } - - let builderTot = builderWorkers.length - idleBuilderWorkers.length; - - // Make the limit on number of builders depends on the available resources - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); - let builderRatio = 1; - for (let 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 (let target of foundations.values()) - { - if (target.hasClass("Field")) - continue; // we do not build fields - - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) - continue; - - // if our territory has shrinked since this foundation was positioned, do not build it - if (m.isNotWorthBuilding(gameState, target)) - continue; - - let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - let maxTotalBuilders = Math.ceil(workers.length * 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.hasClass("Fortress") || target.hasClass("Wonder") || - target.getMetadata(PlayerID, "phaseUp") == true) - targetNB = 7; - else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || - target.hasClass("Market")) - targetNB = 4; - else if (target.hasClass("House") || target.hasClass("DropsiteWood")) - targetNB = 3; - - if (target.getMetadata(PlayerID, "baseAnchor") == true || - target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) - { - targetNB = 15; - maxTotalBuilders = Math.max(maxTotalBuilders, 15); - } - - // if no base yet, everybody should build - if (gameState.ai.HQ.numActiveBases() == 0) - { - targetNB = workers.length; - maxTotalBuilders = targetNB; - } - - if (assigned >= targetNB) - continue; - idleBuilderWorkers.forEach(function(ent) { - if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (assigned >= targetNB || !ent.position() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }).toEntityArray(); - let time = target.buildTime(); - nonBuilderWorkers.sort((workerA, workerB) => { - let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); - // elephant moves slowly, so when far away they are only useful if build time is long - if (workerA.hasClass("Elephant")) - coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); - else if (workerA.getMetadata(PlayerID, "gather-type") == "food") - coeffA *= 3; - let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); - if (workerB.hasClass("Elephant")) - coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); - else if (workerB.getMetadata(PlayerID, "gather-type") == "food") - coeffB *= 3; - return coeffA - coeffB; - }); - let current = 0; - let nonBuilderTot = nonBuilderWorkers.length; - while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) - { - ++assigned; - ++builderTot; - let ent = nonBuilderWorkers[current++]; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - } - } - - for (let target of damagedBuildings.values()) - { - // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building - // that's being destroyed. - if (gameState.ai.HQ.isNearInvadingArmy(target.position())) - { - if (target.healthLevel() > 0.5 || - !target.hasClass("CivCentre") && !target.hasClass("StoneWall") && - (!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.hasClass("Fortress") || target.hasClass("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() || - API3.SquareVectorDistance(ent.position(), target.position()) > 40000) - return; - ++assigned; - ++builderTot; - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - if (assigned >= targetNB || builderTot >= maxTotalBuilders) - continue; - let nonBuilderWorkers = workers.filter(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") == "builder") - return false; - if (!ent.position()) - return false; - if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return false; - if (ent.getMetadata(PlayerID, "transport")) - return false; - return true; - }); - let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); - let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); - - nearestNonBuilders.forEach(function(ent) { - ++assigned; - ++builderTot; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target.id()); - }); - } -}; - -/** Return false when the base is not active (no workers on it) */ -m.BaseManager.prototype.update = function(gameState, queues, events) -{ - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units - { - // if some active base, reassigns the workers/buildings - // otherwise look for anything useful to do, i.e. treasures to gather - if (gameState.ai.HQ.numActiveBases() > 0) - { - for (let ent of this.units.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (bestBase.ID != this.ID) - bestBase.assignEntity(gameState, ent); - } - for (let ent of this.buildings.values()) - { - let bestBase = m.getBestBase(gameState, ent); - if (!bestBase) - { - if (ent.hasClass("Dock")) - API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); - continue; - } - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - 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 (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let 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 (let ent of this.units.values()) - { - let base = m.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 (let ent of this.buildings.values()) - { - if (!ent.position()) - continue; - let base = m.getBestBase(gameState, ent); - if (base.anchor) - reassignedBase = base; - break; - } - - if (reassignedBase) - { - for (let ent of this.units.values()) - reassignedBase.assignEntity(gameState, ent); - for (let ent of this.buildings.values()) - { - if (ent.resourceDropsiteTypes()) - this.removeDropsite(gameState, ent); - reassignedBase.assignEntity(gameState, ent); - } - return false; - } - - this.assignToFoundations(gameState); - if (gameState.ai.elapsedTime > this.timeNextIdleCheck) - this.setWorkersIdleByPriority(gameState); - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - return true; - } - - Engine.ProfileStart("Base update - base " + this.ID); - - this.checkResourceLevels(gameState, queues); - this.assignToFoundations(gameState); - - if (this.constructing) - { - let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); - if(owner != 0 && !gameState.isPlayerAlly(owner)) - { - // we're in enemy territory. If we're too close from the enemy, destroy us. - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (cc.owner() != owner) - continue; - if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) - continue; - this.anchor.destroy(); - gameState.ai.HQ.resetBaseCache(); - break; - } - } - } - else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) - --this.neededDefenders; - - if (gameState.ai.elapsedTime > this.timeNextIdleCheck && - (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) - this.setWorkersIdleByPriority(gameState); - - this.assignRolelessUnits(gameState); - this.reassignIdleWorkers(gameState); - // check if workers can find something useful to do - for (let ent of this.workers.values()) - this.workerObject.update(gameState, ent); - for (let ent of this.mobileDropsites.values()) - this.workerObject.moveToGatherer(gameState, ent, false); - - Engine.ProfileStop(); - return true; -}; - -m.BaseManager.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "anchorId": this.anchorId, - "accessIndex": this.accessIndex, - "maxDistResourceSquare": this.maxDistResourceSquare, - "constructing": this.constructing, - "gatherers": this.gatherers, - "neededDefenders": this.neededDefenders, - "territoryIndices": this.territoryIndices, - "timeNextIdleCheck": this.timeNextIdleCheck - }; -}; - -m.BaseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-unitary/buildManager.js b/install/petraBased/petra-unitary/buildManager.js deleted file mode 100644 index fa78ca2..0000000 --- a/install/petraBased/petra-unitary/buildManager.js +++ /dev/null @@ -1,173 +0,0 @@ -var PETRA = function(m) -{ - -/** - * 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. - */ - -m.BuildManager = function() -{ - // List of buildings we have builders for, with number of possible builders. - this.builderCounters = 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(); -}; - -/** Initialization at start of game */ -m.BuildManager.prototype.init = function(gameState) -{ - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnUnits().values()) - this.incrementBuilderCounters(civ, ent, 1); -}; - -m.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) -{ - for (let buildable of ent.buildableEntities(civ)) - { - if (this.builderCounters.has(buildable)) - { - let count = this.builderCounters.get(buildable) + increment; - if (count < 0) - { - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); - continue; - } - this.builderCounters.set(buildable, count); - } - else if (increment > 0) - this.builderCounters.set(buildable, increment); - else - API3.warn(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); - } -}; - -/** Update the builders counters */ -m.BuildManager.prototype.checkEvents = function(gameState, events) -{ - this.elapsedTime = gameState.ai.elapsedTime; - let civ = gameState.getPlayerCiv(); - - for (let evt of events.Create) - { - if (events.Destroy.some(e => e.entity == evt.entity)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, 1); - } - - for (let evt of events.Destroy) - { - if (events.Create.some(e => e.entity == evt.entity) || !evt.entityObj) - continue; - let ent = evt.entityObj; - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, -1); - } - - for (let evt of events.OwnershipChanged) // capture events - { - let increment; - if (evt.from == PlayerID) - increment = -1; - else if (evt.to == PlayerID) - increment = 1; - else - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("Unit")) - this.incrementBuilderCounters(civ, ent, increment); - } -}; - - -/** - * Get the first buildable structure with a given class - * TODO when several available, choose the best one - */ -m.BuildManager.prototype.findStructureWithClass = function(gameState, classes) -{ - for (let [templateName, count] of this.builderCounters) - { - if (count == 0 || gameState.isTemplateDisabled(templateName)) - continue; - let template = gameState.getTemplate(templateName); - if (!template || !template.available(gameState)) - continue; - if (MatchesClassList(template.classes(), classes)) - return templateName; - } - return undefined; -}; - -m.BuildManager.prototype.hasBuilder = function(template) -{ - let numBuilders = this.builderCounters.get(template); - return numBuilders && numBuilders > 0; -}; - -m.BuildManager.prototype.isUnbuildable = function(gameState, template) -{ - return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; -}; - -m.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 */ -m.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 - { - let 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 */ -m.BuildManager.prototype.numberMissingRoom = function(gameState) -{ - let num = 0; - for (let unbuildable of this.unbuildables.values()) - if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) - ++num; - return num; -}; - -/** Reset the unbuildables due to missing room */ -m.BuildManager.prototype.resetMissingRoom = function(gameState) -{ - for (let [key, unbuildable] of this.unbuildables) - if (unbuildable.reason == "room") - this.unbuildables.delete(key); -}; - -m.BuildManager.prototype.Serialize = function() -{ - return { - "builderCounters": this.builderCounters, - "unbuildables": this.unbuildables - }; -}; - -m.BuildManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/chatHelper.js b/install/petraBased/petra-unitary/chatHelper.js deleted file mode 100644 index 8afbcfc..0000000 --- a/install/petraBased/petra-unitary/chatHelper.js +++ /dev/null @@ -1,242 +0,0 @@ -var PETRA = function(m) -{ - -m.launchAttackMessages = { - "hugeAttack": [ - markForTranslation("I am starting a massive military campaign against %(_player_)s, come and join me."), - markForTranslation("I have set up a huge army to crush %(_player_)s. Join me and you will have your share of the loot.") - ], - "other": [ - markForTranslation("I am launching an attack against %(_player_)s."), - markForTranslation("I have just sent an army against %(_player_)s.") - ] -}; - -m.answerRequestAttackMessages = { - "join": [ - markForTranslation("Let me regroup my army and I will then join you against %(_player_)s."), - markForTranslation("I am finishing preparations to attack %(_player_)s.") - ], - "decline": [ - markForTranslation("Sorry, I do not have enough soldiers currently; but my next attack will target %(_player_)s."), - markForTranslation("Sorry, I still need to strengthen my army. However, I will attack %(_player_)s next.") - ], - "other": [ - markForTranslation("I cannot help you against %(_player_)s for the time being, I am planning to attack %(_player_2)s first.") - ] -}; - -m.sentTributeMessages = [ - markForTranslation("Here is a gift for you, %(_player_)s. Make good use of it."), - markForTranslation("I see you are in a bad situation, %(_player_)s. I hope this helps."), - markForTranslation("I can help you this time, %(_player_)s, but you should manage your resources more carefully in the future.") -]; - -m.requestTributeMessages = [ - markForTranslation("I am in need of %(resource)s, can you help? I will make it up to you."), - markForTranslation("I would participate more efficiently in our common war effort if you could provide me some %(resource)s."), - markForTranslation("If you can spare me some %(resource)s, I will be able to strengthen my army.") -]; - -m.newTradeRouteMessages = [ - markForTranslation("I have set up a new route with %(_player_)s. Trading will be profitable for all of us."), - markForTranslation("A new trade route is set up with %(_player_)s. Take your share of the profits.") -]; - -m.newDiplomacyMessages = { - "ally": [ - markForTranslation("%(_player_)s and I are now allies.") - ], - "neutral": [ - markForTranslation("%(_player_)s and I are now neutral.") - ], - "enemy": [ - markForTranslation("%(_player_)s and I are now enemies.") - ] -}; - -m.answerDiplomacyRequestMessages = { - "ally": { - "decline": [ - markForTranslation("I cannot accept your offer to become allies, %(_player_)s.") - ], - "declineSuggestNeutral": [ - markForTranslation("I will not be your ally, %(_player_)s. However, I will consider a neutrality pact."), - markForTranslation("I reject your request for alliance, %(_player_)s, but we could become neutral."), - markForTranslation("%(_player_)s, only a neutrality agreement is conceivable to me.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous alliance did not work out, %(_player_)s. I must decline your offer."), - markForTranslation("I won’t ally you again, %(_player_)s!"), - markForTranslation("No more alliances between us, %(_player_)s!"), - markForTranslation("Your request for peace means nothing to me anymore, %(_player_)s!"), - markForTranslation("My answer to your repeated peace proposal will remain war, %(_player_)s!") - ], - "accept": [ - markForTranslation("I will accept your offer to become allies, %(_player_)s. We will both benefit from this partnership."), - markForTranslation("An alliance between us is a good idea, %(_player_)s."), - markForTranslation("Let both of our people prosper from a peaceful association, %(_player_)s."), - markForTranslation("We have found common ground, %(_player_)s. I accept the alliance."), - markForTranslation("%(_player_)s, consider us allies from now on.") - ], - "acceptWithTribute": [ - markForTranslation("I will ally with you, %(_player_)s, but only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("%(_player_)s, you must send me a tribute of %(_amount_)s %(_resource_)s before I accept an alliance with you."), - markForTranslation("Unless you send me %(_amount_)s %(_resource_)s, an alliance won’t be formed, %(_player_)s,") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, my offer still stands. I will ally with you only if you send me a tribute of %(_amount_)s %(_resource_)s."), - markForTranslation("I’m still waiting for %(_amount_)s %(_resource_)s before accepting your alliance, %(_player_)s."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - }, - "neutral": { - "decline": [ - markForTranslation("I will not become neutral with you, %(_player_)s."), - markForTranslation("%(_player_)s, I must decline your request for a neutrality pact.") - ], - "declineRepeatedOffer": [ - markForTranslation("Our previous neutrality agreement ended in failure, %(_player_)s; I will not consider another one.") - ], - "accept": [ - markForTranslation("I welcome your request for peace between our civilizations, %(_player_)s. I will accept."), - markForTranslation("%(_player_)s, I will accept your neutrality request. May both our civilizations benefit.") - ], - "acceptWithTribute": [ - markForTranslation("If you send me a tribute of %(_amount_)s %(_resource_)s, I will accept your neutrality request, %(_player_)s."), - markForTranslation("%(_player_)s, if you send me %(_amount_)s %(_resource_)s, I will accept a neutrality pact.") - ], - "waitingForTribute": [ - markForTranslation("%(_player_)s, I will not accept your neutrality request unless you tribute me %(_amount_)s %(_resource_)s soon."), - markForTranslation("%(_player_)s, if you do not send me part of the %(_amount_)s %(_resource_)s tribute soon, I will break off our negotiations.") - ] - } -}; - -m.sendDiplomacyRequestMessages = { - "ally": { - "sendRequest": [ - markForTranslation("%(_player_)s, it would help both of our civilizations if we formed an alliance. If you become allies with me, I will respond in kind.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, my offer for an alliance has expired."), - markForTranslation("%(_player_)s, I have rescinded my previous offer for an alliance between us."), - ] - }, - "neutral": { - "sendRequest": [ - markForTranslation("%(_player_)s, I would like to request a neutrality pact between our civilizations. If you become neutral with me, I will respond in kind."), - markForTranslation("%(_player_)s, it would be both to our benefit if we negotiated a neutrality pact. I will become neutral with you if you do the same.") - ], - "requestExpired": [ - markForTranslation("%(_player_)s, I have decided to revoke my offer for a neutrality pact."), - markForTranslation("%(_player_)s, as you have failed to respond to my request for peace between us, I have abrogated my offer."), - ] - } -}; - -m.chatLaunchAttack = function(gameState, player, type) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.launchAttackMessages[type === "HugeAttack" ? "hugeAttack" : "other"]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestAttack = function(gameState, player, answer, other) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.answerRequestAttackMessages[answer]), - "translateMessage": true, - "translateParameters": answer != "other" ? ["_player_"] : ["_player_", "_player_2"], - "parameters": answer != "other" ? { "_player_": player } : { "_player_": player, "_player_2": other } - }); -}; - -m.chatSentTribute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.sentTributeMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatRequestTribute = function(gameState, resource) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.requestTributeMessages), - "translateMessage": true, - "translateParameters": { "resource": "withinSentence" }, - "parameters": { "resource": Resources.GetNames()[resource] } - }); -}; - -m.chatNewTradeRoute = function(gameState, player) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newTradeRouteMessages), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatNewPhase = function(gameState, phase, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/allies " + pickRandom(this.newPhaseMessages[status]), - "translateMessage": true, - "translateParameters": ["phase"], - "parameters": { "phase": phase } - }); -}; - -m.chatNewDiplomacy = function(gameState, player, newDiplomaticStance) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": pickRandom(this.newDiplomacyMessages[newDiplomaticStance]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -m.chatAnswerRequestDiplomacy = function(gameState, player, requestType, response, requiredTribute) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.answerDiplomacyRequestMessages[requestType][response]), - "translateMessage": true, - "translateParameters": requiredTribute ? ["_amount_", "_resource_", "_player_"] : ["_player_"], - "parameters": requiredTribute ? - { "_amount_": requiredTribute.wanted, "_resource_": requiredTribute.type, "_player_": player } : - { "_player_": player } - }); -}; - -m.chatNewRequestDiplomacy = function(gameState, player, requestType, status) -{ - Engine.PostCommand(PlayerID, { - "type": "aichat", - "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + - pickRandom(this.sendDiplomacyRequestMessages[requestType][status]), - "translateMessage": true, - "translateParameters": ["_player_"], - "parameters": { "_player_": player } - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/config.js b/install/petraBased/petra-unitary/config.js deleted file mode 100644 index 77e4b56..0000000 --- a/install/petraBased/petra-unitary/config.js +++ /dev/null @@ -1,257 +0,0 @@ -var PETRA = function(m) -{ - -m.Config = function(difficulty, behavior) -{ - // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. - this.difficulty = difficulty !== undefined ? difficulty : 3; - - // 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; - - this.chat = true; // false to prevent AI's chats - - this.popScaling = 1; // scale factor depending on the max population - - this.Military = { - "towerLapseTime": 90, // Time to wait between building 2 towers - "fortressLapseTime": 390, // Time to wait between building 2 fortresses - "popForBarracks1": 25, - "popForBarracks2": 95, - "popForBlacksmith": 65, - "numSentryTowers": 1 - }; - this.Economy = { - "popPhase2": 38, // How many units we want before aging to phase2. - "workPhase3": 65, // How many workers we want before aging to phase3. - "workPhase4": 80, // How many workers we want before aging to phase4 or higher. - "popForDock": 25, - "targetNumWorkers": 40, // dummy, will be changed later - "targetNumTraders": 5, // Target number of traders - "targetNumFishers": 1, // Target number of fishers per sea - "supportRatio": 0.35, // fraction of support workers among the workforce - "provisionFields": 2 - }; - - // Note: attack settings are set directly in attack_plan.js - // defense - this.Defense = - { - "defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. - "armyCompactSize": 2000, // squared. Half-diameter of an army. - "armyBreakawaySize": 3500, // squared. - "armyMergeSize": 1400 // squared. - }; - - // 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 = - { - "default": [], - "athen": ["structures/{civ}_gymnasion", "structures/{civ}_prytaneion", - "structures/{civ}_theatron", "structures/{civ}_royal_stoa"], - "brit": ["structures/{civ}_rotarymill"], - "cart": ["structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", - "structures/{civ}_embassy_italiote"], - "gaul": ["structures/{civ}_rotarymill", "structures/{civ}_tavern"], - "iber": ["structures/{civ}_monument"], - "kush": ["structures/{civ}_pyramid_large", "structures/{civ}_blemmye_camp", - "structures/{civ}_nuba_village"], - "mace": ["structures/{civ}_library", "structures/{civ}_theatron"], - "maur": ["structures/{civ}_pillar_ashoka"], - "pers": ["structures/{civ}_apadana", "structures/{civ}_hall"], - "ptol": ["structures/{civ}_library"], - "rome": ["structures/{civ}_army_camp"], - "sele": ["structures/{civ}_library"], - "spart": ["structures/{civ}_syssiton", "structures/{civ}_theatron", - "structures/{civ}_royal_stoa"] - }; - - this.priorities = - { - "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food - "citizenSoldier": 60, - "trader": 50, - "healer": 20, - "ships": 70, - "house": 350, - "dropsites": 200, - "field": 400, - "dock": 90, - "corral": 100, - "economicBuilding": 90, - "militaryBuilding": 130, - "defenseBuilding": 70, - "civilCentre": 950, - "majorTech": 700, - "minorTech": 40, - "wonder": 1000, - "emergency": 1000 // used only in emergency situations, should be the highest one - }; - - // Default personality (will be updated in setConfig) - this.personality = - { - "aggressive": 0.5, - "cooperative": 0.5, - "defensive": 0.5 - }; - - // See m.QueueManager.prototype.wantedGatherRates() - this.queues = - { - "firstTurn": { - "food": 10, - "wood": 10, - "default": 0 - }, - "short": { - "food": 200, - "wood": 200, - "default": 100 - }, - "medium": { - "default": 0 - }, - "long": { - "default": 0 - } - }; - - this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; -}; - -m.Config.prototype.setConfig = function(gameState) -{ - if (this.difficulty > 0) - { - // 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 - let 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 } - }; - let behavior = randFloat(-0.5, 0.5); - // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety - let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); - let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; - let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; - let min = personalityList[this.behavior].min; - let 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 < 2) - { - this.Economy.supportRatio = 0.5; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else if (this.difficulty < 3) - { - this.Economy.supportRatio = 0.4; - this.Economy.provisionFields = 1; - this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; - } - else - { - if (this.difficulty == 3) - 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; - } - } - - let maxPop = gameState.getPopulationMax(); - if (this.difficulty < 2) - this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); - else if (this.difficulty < 3) - this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); - else - this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); - this.Economy.targetNumTraders = 2 + this.difficulty; - - - if (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.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * 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 < 2) - this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase - - if (this.debug < 2) - return; - API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); -}; - -m.Config.prototype.Serialize = function() -{ - var data = {}; - for (let key in this) - if (this.hasOwnProperty(key) && key != "debug") - data[key] = this[key]; - return data; -}; - -m.Config.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/defenseArmy.js b/install/petraBased/petra-unitary/defenseArmy.js deleted file mode 100644 index a5be2f2..0000000 --- a/install/petraBased/petra-unitary/defenseArmy.js +++ /dev/null @@ -1,651 +0,0 @@ -var PETRA = function(m) -{ - -/** Armies used by the defense manager. - * An army is a collection of own entities and enemy entities. - * - * Types of armies: - * "default": army to counter an invading army - * "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 - */ -m.DefenseArmy = function(gameState, foeEntities, type) -{ - 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 (let 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 - */ -m.DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) -{ - if (this.foeEntities.indexOf(enemyId) !== -1) - return false; - let ent = gameState.getEntityById(enemyId); - if (!ent || !ent.position()) - return false; - - // check distance - if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) - return false; - - this.foeEntities.push(enemyId); - this.assignedAgainst[enemyId] = []; - this.positionLastUpdate = 0; - this.evaluateStrength(ent); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - - return true; -}; - -/** - * returns true if the entity was removed and false otherwise. - * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. - */ -m.DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) -{ - let idx = this.foeEntities.indexOf(enemyId); - if (idx === -1) - return false; - - this.foeEntities.splice(idx, 1); - - this.assignedAgainst[enemyId] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] == enemyId) - this.assignedTo[to] = undefined; - - let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); - if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) - { - this.evaluateStrength(ent, false, true); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - } - - return true; -}; - -/** - * adds a defender but doesn't assign him yet. - * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship - */ -m.DefenseArmy.prototype.addOwn = function(gameState, id, force) -{ - if (this.ownEntities.indexOf(id) !== -1) - return false; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position() && !force) - return false; - - this.ownEntities.push(id); - this.evaluateStrength(ent, true); - ent.setMetadata(PlayerID, "PartOfArmy", this.ID); - this.assignedTo[id] = 0; - - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole === undefined || subrole !== "defender") - ent.setMetadata(PlayerID, "formerSubrole", subrole); - ent.setMetadata(PlayerID, "subrole", "defender"); - return true; -}; - -m.DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) -{ - let idx = this.ownEntities.indexOf(id); - if (idx === -1) - return false; - - this.ownEntities.splice(idx, 1); - - if (this.assignedTo[id] !== 0) - { - let temp = this.assignedAgainst[this.assignedTo[id]]; - if (temp) - temp.splice(temp.indexOf(id), 1); - } - this.assignedTo[id] = undefined; - - let ent = Entity ? Entity : gameState.getEntityById(id); - if (!ent) - 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); - - let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); - if (formerSubrole !== undefined) - ent.setMetadata(PlayerID, "subrole", formerSubrole); - else - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "formerSubrole", undefined); - - // Remove from tranport plan if not yet on Board - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan && plan.state == "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. - */ -m.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 - let posOwn = [0, 0]; - let nOwn = 0; - let posAlly = [0, 0]; - let nAlly = 0; - let posOther = [0, 0]; - let nOther = 0; - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - let pos = ent.position(); - let 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; - let armyAccess = gameState.ai.accessibility.getAccessValue(posOther); - for (let struct of gameState.getAllyStructures().values()) - { - let pos = struct.position(); - if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) - continue; - if (m.getLandAccess(gameState, struct) !== armyAccess) - continue; - let defensiveStruct = struct.hasDefensiveFire(); - if (defensiveFound && !defensiveStruct) - continue; - let dist = API3.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) - { - let entId = this.ownEntities[0]; - this.removeOwn(gameState, entId); - let 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); -}; - -m.DefenseArmy.prototype.assignUnit = function(gameState, entID) -{ - // we'll assume this defender is ours already. - // we'll also override any previous assignment - - let ent = gameState.getEntityById(entID); - if (!ent || !ent.position()) - return false; - - // try to return its resources, and if any, the attack order will be queued - let queued = m.returnResources(gameState, ent); - - let idMin; - let distMin; - let idMinAll; - let distMinAll; - for (let id of this.foeEntities) - { - let eEnt = gameState.getEntityById(id); - if (!eEnt || !eEnt.position()) // probably can't happen. - 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") && !m.isSiegeUnit(eEnt)) - continue; - - let dist = API3.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; - - let ownIndex = m.getLandAccess(gameState, ent); - let foeEnt = gameState.getEntityById(idFoe); - let foePosition = foeEnt.position(); - let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); - if (ownIndex == foeIndex || ent.hasClass("Ship")) - { - this.assignedTo[entID] = idFoe; - this.assignedAgainst[idFoe].push(entID); - ent.attack(idFoe, m.allowCapture(gameState, ent, foeEnt), queued); - } - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); - return true; -}; - -m.DefenseArmy.prototype.getType = function() -{ - return this.type; -}; - -m.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). - */ -m.DefenseArmy.prototype.merge = function(gameState, otherArmy) -{ - // copy over all parameters. - for (let i in otherArmy.assignedAgainst) - { - if (this.assignedAgainst[i] === undefined) - this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; - else - this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); - } - for (let i in otherArmy.assignedTo) - this.assignedTo[i] = otherArmy.assignedTo[i]; - - for (let id of otherArmy.foeEntities) - this.addFoe(gameState, id, true); - // TODO: reassign those ? - for (let id of otherArmy.ownEntities) - this.addOwn(gameState, id, true); - - this.recalculatePosition(gameState, true); - this.recalculateStrengths(gameState); - - return true; -}; - -m.DefenseArmy.prototype.needsDefenders = function(gameState) -{ - let defenseRatio; - let 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. */ -m.DefenseArmy.prototype.recalculatePosition = function(gameState, force) -{ - if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) - return; - - let npos = 0; - let pos = [0, 0]; - for (let id of this.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - npos++; - let epos = ent.position(); - pos[0] += epos[0]; - pos[1] += epos[1]; - } - // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position - if (npos > 0) - { - this.foePosition[0] = pos[0]/npos; - this.foePosition[1] = pos[1]/npos; - } - - this.positionLastUpdate = gameState.ai.elapsedTime; -}; - -m.DefenseArmy.prototype.recalculateStrengths = function(gameState) -{ - this.ownStrength = 0; - this.foeStrength = 0; - - for (let id of this.foeEntities) - this.evaluateStrength(gameState.getEntityById(id)); - for (let id of this.ownEntities) - this.evaluateStrength(gameState.getEntityById(id), true); -}; - -/** adds or remove the strength of the entity either to the enemy or to our units. */ -m.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 = m.getMaxStrength(ent); - - // TODO adapt the getMaxStrength function for animals. - // For the time being, just increase it for elephants as the returned value is too small. - if (ent.hasClass("Animal") && ent.hasClass("Elephant")) - entStrength *= 3; - - if (remove) - entStrength *= -1; - - if (isOwn) - this.ownStrength += entStrength; - else - this.foeStrength += entStrength; -}; - -m.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 (let evt of events.EntityRenamed) // take care of promoted and packed units - { - if (this.foeEntities.indexOf(evt.entity) !== -1) - { - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed - continue; - let idx = this.foeEntities.indexOf(evt.entity); - this.foeEntities[idx] = evt.newentity; - this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; - this.assignedAgainst[evt.entity] = undefined; - for (let to in this.assignedTo) - if (this.assignedTo[to] === evt.entity) - this.assignedTo[to] = evt.newentity; - } - else if (this.ownEntities.indexOf(evt.entity) !== -1) - { - let idx = this.ownEntities.indexOf(evt.entity); - this.ownEntities[idx] = evt.newentity; - this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; - this.assignedTo[evt.entity] = undefined; - for (let against in this.assignedAgainst) - { - if (!this.assignedAgainst[against]) - continue; - if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) - this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; - } - } - } - - for (let evt of events.Garrison) - this.removeFoe(gameState, evt.entity); - - for (let evt of events.OwnershipChanged) // captured - { - if (!gameState.isPlayerEnemy(evt.to)) - this.removeFoe(gameState, evt.entity); - else if (evt.from === PlayerID) - this.removeOwn(gameState, evt.entity); - } - - for (let evt of events.Destroy) - { - let entityObj = evt.entityObj || undefined; - // we may have capture+destroy, so do not trust owner and check all possibilities - this.removeOwn(gameState, evt.entity, entityObj); - this.removeFoe(gameState, evt.entity, entityObj); - } -}; - -m.DefenseArmy.prototype.update = function(gameState) -{ - for (let entId of this.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent) - continue; - let 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") - { - let target = gameState.getEntityById(orderData[0].target); - if (target && !m.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) - let 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 []; - } - - let breakaways = []; - // TODO: assign unassigned defenders, cleanup of a few things. - // perhaps occasional strength recomputation - - // occasional update or breakaways, positions… - if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) - { - this.recalculatePosition(gameState); - this.positionLastUpdate = gameState.ai.elapsedTime; - - // Check for breakaways. - for (let i = 0; i < this.foeEntities.length; ++i) - { - let id = this.foeEntities[i]; - let ent = gameState.getEntityById(id); - if (!ent || !ent.position()) - continue; - if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) - { - breakaways.push(id); - if (this.removeFoe(gameState, id)) - i--; - } - } - - this.recalculatePosition(gameState); - } - - return breakaways; -}; - -m.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 - }; -}; - -m.DefenseArmy.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/defenseManager.js b/install/petraBased/petra-unitary/defenseManager.js deleted file mode 100644 index 0d18897..0000000 --- a/install/petraBased/petra-unitary/defenseManager.js +++ /dev/null @@ -1,957 +0,0 @@ -var PETRA = function(m) -{ - -m.DefenseManager = function(Config) -{ - this.armies = []; // array of "army" Objects - 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 = {}; -}; - -m.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) - { - let 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 = {}; - let attackingArmies = clone(this.attackingArmies); - for (let enemy in this.attackingUnits) - { - if (!this.attackingUnits[enemy]) - continue; - for (let ally in this.attackingUnits[enemy]) - { - 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 (let enemy in attackingArmies) - { - for (let 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(); -}; - -m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") -{ - if (type == "default") - { - // Try to add it to an existing army. - for (let army of this.armies) - if (army.getType() == type && army.addFoe(gameState, entityID)) - return; // over - } - - // Create a new army for it. - let army = new m.DefenseArmy(gameState, [entityID], type); - - this.armies.push(army); -}; - -m.DefenseManager.prototype.getArmy = function(partOfArmy) -{ - // Find the army corresponding to this ID partOfArmy - for (let army of this.armies) - if (army.ID == partOfArmy) - return army; - - return undefined; -}; - -m.DefenseManager.prototype.isDangerous = function(gameState, entity) -{ - if (!entity.position()) - return false; - - let 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") - { - let targetId = entity.unitAIOrderData()[0].target; - if (this.targetList.indexOf(targetId) != -1) - return true; - let target = gameState.getEntityById(targetId); - if (target) - { - let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); - if (isTargetEnemy && territoryOwner == PlayerID) - { - if (target.hasClass("Structure")) - this.targetList.push(targetId); - return true; - } - else if (isTargetEnemy && target.hasClass("CivCentre")) - { - let myBuildings = gameState.getOwnStructures(); - for (let building of myBuildings.values()) - { - if (building.foundationProgress() == 0) - continue; - if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) - continue; - this.targetList.push(targetId); - return true; - } - } - } - } - - 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 (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position()) // the enemy base is either destroyed or built - continue; - if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) - return true; - } - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) - continue; - let cooperation = this.GetCooperationLevel(cc.owner()); - if (cooperation < 0.6 && cc.foundationProgress() !== undefined) - continue; - if (cooperation < 0.3) - continue; - if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) - return true; - } - - for (let building of gameState.getOwnStructures().values()) - { - if (building.foundationProgress() == 0 || - API3.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 (let building of gameState.getAllyStructures(territoryOwner).values()) - { - if (building.foundationProgress() == 0 || - API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) - continue; - if (!this.territoryMap.isBlinking(building.position())) - return true; - } - } - - // Update the number of enemies attacking this ally - let 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; -}; - -m.DefenseManager.prototype.checkEnemyUnits = function(gameState) -{ - const nbPlayers = gameState.sharedScript.playersData.length; - let i = gameState.ai.playedTurn % nbPlayers; - this.attackingUnits[i] = undefined; - - if (i == PlayerID) - { - if (!this.armies.length) - { - // check if we can recover capture points from any of our notdecaying structures - for (let ent of gameState.getOwnStructures().values()) - { - if (ent.decaying()) - continue; - let 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; - - // loop through enemy units - for (let 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; - let orders = ent.unitAIOrderData(); - if (!orders || !orders.length || !orders[0].target) - continue; - let target = gameState.getEntityById(orders[0].target); - if (!target || !gameState.isPlayerAlly(target.owner())) - continue; - } - - // TODO what to do for ships ? - if (ent.hasClass("Ship") || ent.hasClass("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.numActiveBases() == 0) - 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 (let ent of gameState.getEnemyStructures(i).values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) - continue; - if (!ent.capturePoints() && !ent.hasDefensiveFire()) - continue; - let owner = this.territoryMap.getOwner(ent.position()); - if (owner == PlayerID) - this.makeIntoArmy(gameState, ent.id(), "capturing"); - } -}; - -m.DefenseManager.prototype.checkEnemyArmies = function(gameState) -{ - for (let i = 0; i < this.armies.length; ++i) - { - let army = this.armies[i]; - // this returns a list of IDs: the units that broke away from the army for being too far. - let breakaways = army.update(gameState); - for (let breaker of breakaways) - this.makeIntoArmy(gameState, breaker); // assume dangerosity - - if (army.getState() == 0) - { - if (army.getType() == "default") - this.switchToAttack(gameState, army); - army.clear(gameState); - this.armies.splice(i--, 1); - continue; - } - } - // Check if we can't merge it with another - for (let i = 0; i < this.armies.length - 1; ++i) - { - let army = this.armies[i]; - if (army.getType() != "default") - continue; - for (let j = i+1; j < this.armies.length; ++j) - { - let otherArmy = this.armies[j]; - if (otherArmy.getType() != "default" || - API3.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) - { - let army = this.armies[i]; - army.recalculatePosition(gameState); - let owner = this.territoryMap.getOwner(army.foePosition); - if (!gameState.isPlayerEnemy(owner)) - { - if (gameState.isPlayerMutualAlly(owner)) - { - // update the number of enemies attacking this ally - for (let id of army.foeEntities) - { - let ent = gameState.getEntityById(id); - if (!ent) - continue; - let 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; - } - else if (owner != 0) // enemy army back in its territory - { - 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; - let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - for (let base of bases.values()) - { - if (!gameState.isEntityAlly(base)) - continue; - let cooperation = this.GetCooperationLevel(base.owner()); - if (cooperation < 0.3 && !gameState.isEntityOwn(base)) - continue; - if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) - continue; - if(this.Config.debug > 1) - API3.warn("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 (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (API3.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); - } -}; - -m.DefenseManager.prototype.assignDefenders = function(gameState) -{ - if (!this.armies.length) - return; - - let armiesNeeding = []; - // let's add defenders - for (let army of this.armies) - { - let needsDef = army.needsDefenders(gameState); - if (needsDef === false) - continue; - - let armyAccess; - for (let entId of army.foeEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - armyAccess = m.getLandAccess(gameState, ent); - break; - } - if (!armyAccess) - API3.warn(" 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 - let 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.hasClass("Catapult")) - return; - if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) - 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) - { - let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole == "completing" || subrole == "walking" || 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) - { - let ent = gameState.getEntityById(potentialDefenders[i]); - if (!ent || !ent.position()) - continue; - let aMin; - let distMin; - let access = ipass == 0 ? m.getLandAccess(gameState, ent) : undefined; - for (let a = 0; a < armiesNeeding.length; ++a) - { - if (access && armiesNeeding[a].access != access) - continue; - let dist = API3.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 -= m.getMaxStrength(ent); - 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 centre - let armiesPos = []; - for (let a = 0; a < armiesNeeding.length; ++a) - armiesPos.push(armiesNeeding[a].army.foePosition); - gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); -}; - -m.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 - */ -m.DefenseManager.prototype.checkEvents = function(gameState, events) -{ - // must be called every turn for all armies - for (let army of this.armies) - army.checkEvents(gameState, events); - - for (let evt of events.OwnershipChanged) // capture events - { - if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) - { - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.hasClass("CivCentre")) // one of our cc has been captured - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); - } - } - - let allAttacked = {}; - for (let evt of events.Attacked) - allAttacked[evt.target] = evt.attacker; - - for (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position()) - continue; - - let 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")) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (target.isIdle() || unitAIStateOrder == "GATHER") - { - let pos = attacker.position(); - let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; - if (range * range > API3.SquareVectorDistance(pos, target.position())) - target.moveToRange(pos[0], pos[1], range, range); - } - continue; - } - - if (target.hasClass("Ship")) // TODO integrate other ships later, need to be sure it is accessible - 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")) - { - let 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)) - { - let capture = target.capturePoints(); - if (!capture) - continue; - let 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 - let plan = target.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && attack.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.hasClass("Structure") && !attacker.hasClass("Ship")) - { - let territoryOwner = this.territoryMap.getOwner(attacker.position()); - if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) - this.makeIntoArmy(gameState, attacker.id()); - } - - if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) - { - let 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; - let 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 healthlevel - 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 catapult - if (target.hasClass("Catapult") && - !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 if we should retaliate or continue our task - if (target.hasClass("Support") || target.attackTypes() === undefined) - continue; - let orderData = target.unitAIOrderData(); - let currentTarget = orderData && orderData.length && orderData[0].target ? - gameState.getEntityById(orderData[0].target) : undefined; - if (currentTarget) - { - let unitAIState = target.unitAIState(); - let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || - !currentTarget.hasClass("Structure") && !currentTarget.hasClass("Support"))) - continue; - if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) - continue; - if (unitAIStateOrder == "COMBAT" && !m.isSiegeUnit(currentTarget) && - gameState.ai.HQ.capturableTargets.has(orderData[0].target)) - { - // take the nearest unit also attacking this structure to help us - let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); - let minDist; - let minEnt; - let pos = attacker.position(); - capturableTarget.ents.delete(target.id()); - for (let entId of capturableTarget.ents) - { - if (allAttacked[entId]) - continue; - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) - continue; - // Check that the unit is still attacking the structure (since the last played turn) - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let entOrderData = ent.unitAIOrderData(); - if (!entOrderData || !entOrderData.length || !entOrderData[0].target || - entOrderData[0].target != orderData[0].target) - continue; - let dist = API3.SquareVectorDistance(pos, ent.position()); - if (minEnt && dist > minDist) - continue; - minDist = dist; - minEnt = ent; - } - if (minEnt) - { - capturableTarget.ents.delete(minEnt.id()); - minEnt.attack(attacker.id(), m.allowCapture(gameState, minEnt, attacker)); - } - } - } - target.attack(attacker.id(), m.allowCapture(gameState, target, attacker)); - } - } -}; - -m.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) -{ - if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) - return false; - let minGarrison = data.min || target.garrisonMax(); - if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - return false; - if (data.attacker) - { - let attackTypes = target.attackTypes(); - if (!attackTypes || attackTypes.indexOf("Ranged") == -1) - return false; - let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); - let range = target.attackRange("Ranged").max; - if (dist >= range*range) - return false; - } - let access = m.getLandAccess(gameState, target); - let garrisonManager = gameState.ai.HQ.garrisonManager; - let garrisonArrowClasses = target.getGarrisonArrowClasses(); - let typeGarrison = data.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") : !m.isSiegeUnit(data.attacker); - else - allowMelee = true; - } - let units = gameState.getOwnUnits().filter(ent => { - if (!ent.position()) - return false; - if (!MatchesClassList(ent.classes(), garrisonArrowClasses)) - return false; - if (typeGarrison != "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) - return false; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return false; - let 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) - { - let 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 != "decay" && subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(target.position()); - - let ret = false; - for (let ent of units.values()) - { - if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) - break; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - let 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; -}; - -/** garrison a attacked siege ranged unit inside the nearest fortress */ -m.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (nearest) - garrisonManager.garrison(gameState, unit, nearest, "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 - */ -m.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) -{ - let distmin = Math.min(); - let nearest; - let unitAccess = m.getLandAccess(gameState, unit); - let garrisonManager = gameState.ai.HQ.garrisonManager; - for (let ent of gameState.getAllyStructures().values()) - { - if (!ent.isGarrisonHolder()) - continue; - if (!emergency && !ent.buffHeal()) - continue; - if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) - continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && - (!emergency || !ent.garrisoned().length)) - continue; - if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) - continue; - if (m.getLandAccess(gameState, ent) != unitAccess) - continue; - let dist = API3.SquareVectorDistance(ent.position(), unit.position()); - if (dist > distmin) - continue; - distmin = dist; - nearest = ent; - } - if (!nearest) - return false; - - if (!emergency) - { - garrisonManager.garrison(gameState, unit, nearest, "protection"); - return true; - } - if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent - nearest.unload(nearest.garrisoned()[0]); - - garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); - return true; -}; - -/** - * Be more inclined to help an ally attacked by several enemies - */ -m.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 - */ -m.DefenseManager.prototype.switchToAttack = function(gameState, army) -{ - if (!army) - return; - for (let targetId of this.targetList) - { - let target = gameState.getEntityById(targetId); - if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) - continue; - let targetAccess = m.getLandAccess(gameState, target); - let targetPos = target.position(); - for (let entId of army.ownEntities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.position() || m.getLandAccess(gameState, ent) != targetAccess) - continue; - if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) - continue; - gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); - return; - } - } -}; - -m.DefenseManager.prototype.Serialize = function() -{ - let properties = { - "targetList": this.targetList, - "armyMergeSize": this.armyMergeSize, - "attackingUnits": this.attackingUnits, - "attackingArmies": this.attackingArmies, - "attackedAllies": this.attackedAllies - }; - - let armies = []; - for (let army of this.armies) - armies.push(army.Serialize()); - - return { "properties": properties, "armies": armies }; -}; - -m.DefenseManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.armies = []; - for (let dataArmy of data.armies) - { - let army = new m.DefenseArmy(gameState, []); - army.Deserialize(dataArmy); - this.armies.push(army); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/diplomacyManager.js b/install/petraBased/petra-unitary/diplomacyManager.js deleted file mode 100644 index 5b8e259..0000000 --- a/install/petraBased/petra-unitary/diplomacyManager.js +++ /dev/null @@ -1,559 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the diplomacy: - * update our cooperative trait - * sent tribute to allies - * decide which player to turn against in "Last Man Standing" mode - * respond to diplomacy requests - * send diplomacy requests to other players (rarely) - */ - -/** - * If a player sends us an ally or neutral request, an Object in this.receivedDiplomacyRequests will be created - * that includes the request status, and the amount and type of the resource tribute (if any) - * that they must send in order for us to accept their request. - * In addition, a message will be sent if the player has not sent us a tribute within a minute. - * If two minutes pass without a tribute, we will decline their request. - * - * If we send a diplomacy request to another player, an Object in this.sentDiplomacyRequests will be created, - * which consists of the requestType (i.e. "ally" or "neutral") and the timeSent. A chat message will be sent - * to the other player, and AI players will actually be informed of the request by a DiplomacyRequest event - * sent through AIInterface. It is expected that the other player will change their diplomacy stance to the stance - * that we suggested within a period of time, or else the request will be deleted from this.sentDiplomacyRequests. - */ -m.DiplomacyManager = function(Config) -{ - 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 = 120 + randFloat(10, 100); -}; - -/** - * 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. - */ -m.DiplomacyManager.prototype.init = function(gameState) -{ - this.lastManStandingCheck(gameState); - - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - 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 - */ -m.DiplomacyManager.prototype.tributes = function(gameState) -{ - this.nextTributeUpdate = gameState.ai.elapsedTime + 30; - let totalResources = gameState.getResources(); - let 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; - let donor = gameState.getAlliedVictory() || gameState.getEntities(i).length < gameState.getOwnEntities().length; - let allyResources = gameState.sharedScript.playersData[i].resourceCounts; - let allyPop = gameState.sharedScript.playersData[i].popCount; - let tribute = {}; - let toSend = false; - for (let res in allyResources) - { - 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); - for (let k = 0; k < 2; ++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); - m.chatRequestTribute(gameState, res); - if (this.Config.debug > 1) - API3.warn("Tribute on " + res + " requested to player " + i); - break; - } - } - } - } - if (!toSend) - continue; - if (this.Config.debug > 1) - API3.warn("Tribute " + uneval(tribute) + " sent to player " + i); - if (this.Config.chat) - m.chatSentTribute(gameState, i); - Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute }); - } -}; - -m.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 (let evt of events.TributeExchanged) - { - if (evt.to === PlayerID && !gameState.isPlayerAlly(evt.from) && this.receivedDiplomacyRequests.has(evt.from)) - { - let request = this.receivedDiplomacyRequests.get(evt.from); - if (request.status === "waitingForTribute") - { - request.wanted -= evt.amounts[request.type]; - - if (request.wanted <= 0) - { - if (this.Config.debug > 1) - API3.warn("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 (let 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 (let evt of events.Attacked) - { - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || - gameState.ai.HQ.territoryMap.getOwner(target.position()) !== PlayerID || - !gameState.isPlayerEnemy(target.owner())) - continue; - let 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 (let 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 - { - let 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; - } - - let 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)) - { - let response = request !== undefined && (request.status === "declinedRequest" || request.status === "allianceBroken") ? - "decline" : "declineSuggestNeutral"; - m.chatAnswerRequestDiplomacy(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 (let evt of events.DiplomacyRequest) - { - if (evt.player !== PlayerID) - continue; - - this.handleDiplomacyRequest(gameState, evt.source, evt.to); - let request = this.receivedDiplomacyRequests.get(evt.source); - if (this.Config.debug > 0) - API3.warn("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 - }); - } - } - - // An AI player we sent a diplomacy request to demanded we send them a tribute - for (let evt of events.TributeRequest) - { - if (evt.player !== PlayerID) - continue; - - let 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 responseTribute = {}; - responseTribute[evt.resourceType] = evt.resourceWanted; - if (this.Config.debug > 0) - API3.warn("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; - } - } -}; - -/** - * 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. - */ -m.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 turnFactor = 0; - 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; - - 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")) - { - let wonder = gameState.getEnemyStructures(i).filter(API3.Filters.byClass("Wonder"))[0]; - if (wonder) - { - let wonderProgess = wonder.foundationProgress(); - if (wonderProgess === undefined) - { - playerToTurnAgainst = i; - break; - } - turnFactor += wonderProgess * 2.5 + this.betrayWeighting; - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - let relicsCount = gameState.updatingGlobalCollection("allRelics", API3.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"); - let request = this.receivedDiplomacyRequests.get(playerToTurnAgainst); - if (request && request.status !== "allianceBroken") - { - if (request.status === "waitingForTribute") - m.chatAnswerRequestDiplomacy(gameState, player, 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. - */ -m.DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player, requestType) -{ - if (gameState.sharedScript.playersData[PlayerID].teamsLocked) - return; - let response; - let requiredTribute; - let request = this.receivedDiplomacyRequests.get(player); - let 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"; - } - 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; - } - } - 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 - { - response = "acceptWithTribute"; - requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState)[0]; - 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 - }); - } - m.chatAnswerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); -}; - -m.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) - API3.warn("diplomacy stance with player " + player + " is now " + newDiplomaticStance); - if (this.Config.chat) - m.chatNewDiplomacy(gameState, player, newDiplomaticStance); -}; - -m.DiplomacyManager.prototype.checkRequestedTributes = function(gameState) -{ - for (let [player, data] of this.receivedDiplomacyRequests) - if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime) - { - if (data.sentWarning) - { - this.receivedDiplomacyRequests.delete(player); - m.chatAnswerRequestDiplomacy(gameState, player, data.requestType, "decline"); - } - else - { - data.sentWarning = true; - data.warnTime = gameState.ai.elapsedTime + 60; - m.chatAnswerRequestDiplomacy(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. - */ -m.DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState) -{ - let player; - let max = 0; - for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) - { - let mutualEnemies = 0; - let 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; - - 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; - - let requestType = gameState.isPlayerNeutral(player) ? "ally" : "neutral"; - - this.sentDiplomacyRequests.set(player, { - "requestType": requestType, - "timeSent": gameState.ai.elapsedTime - }); - - if (this.Config.debug > 0) - API3.warn("Sending diplomacy request to player " + player + " with " + requestType); - Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType }); - m.chatNewRequestDiplomacy(gameState, player, requestType, "sendRequest"); -}; - -m.DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState) -{ - for (let [player, data] of this.sentDiplomacyRequests) - if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources && - gameState.getPopulation() > 70) - { - m.chatNewRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); - this.sentDiplomacyRequests.delete(player); - } -}; - -m.DiplomacyManager.prototype.update = function(gameState, events) -{ - this.checkEvents(gameState, events); - - if (!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) - { - this.sentDiplomacyRequestLapseTime = gameState.ai.elapsedTime + 300 + randFloat(10, 100); - let 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); -}; - -m.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 - }; -}; - -m.DiplomacyManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/entityExtend.js b/install/petraBased/petra-unitary/entityExtend.js deleted file mode 100644 index 3edbdbf..0000000 --- a/install/petraBased/petra-unitary/entityExtend.js +++ /dev/null @@ -1,441 +0,0 @@ -var PETRA = function(m) -{ - -/** returns true if this unit should be considered as a siege unit */ -m.isSiegeUnit = function(ent) -{ - return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee") && ent.hasClass("Champion"); -}; - -/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ -m.getMaxStrength = function(ent, againstClass) -{ - let strength = 0; - let attackTypes = ent.attackTypes(); - if (!attackTypes) - return strength; - - for (let type of attackTypes) - { - if (type == "Slaughter") - continue; - - let attackStrength = ent.attackStrengths(type); - for (let str in attackStrength) - { - let val = parseFloat(attackStrength[str]); - if (againstClass) - val *= ent.getMultiplierAgainst(type, againstClass); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength"); - } - } - - let attackRange = ent.attackRange(type); - if (attackRange) - strength += attackRange.max * 0.0125; - - let attackTimes = ent.attackTimes(type); - for (let str in attackTimes) - { - let val = parseFloat(attackTimes[str]); - switch (str) - { - case "repeat": - strength += val / 100000; - break; - case "prepare": - strength -= val / 100000; - break; - default: - API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); - } - } - } - - let armourStrength = ent.armourStrengths(); - for (let str in armourStrength) - { - let val = parseFloat(armourStrength[str]); - switch (str) - { - case "Crush": - strength += val * 0.085 / 3; - break; - case "Hack": - strength += val * 0.075 / 3; - break; - case "Pierce": - strength += val * 0.065 / 3; - break; - default: - API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength"); - } - } - - return strength * ent.maxHitpoints() / 100.0; -}; - -/** Get access and cache it (except for units as it can change) in metadata if not already done */ -m.getLandAccess = function(gameState, ent) -{ - if (ent.hasClass("Unit")) - return gameState.ai.accessibility.getAccessValue(ent.position()); - - let access = ent.getMetadata(PlayerID, "access"); - if (!access) - { - access = gameState.ai.accessibility.getAccessValue(ent.position()); - // Docks are sometimes not as expected - if (access < 2 && ent.buildPlacementType() == "shore") - { - let halfDepth = 0; - if (ent.get("Footprint/Square")) - halfDepth = +ent.get("Footprint/Square/@depth") / 2; - else if (ent.get("Footprint/Circle")) - halfDepth = +ent.get("Footprint/Circle/@radius"); - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < halfDepth; d += 3) - { - let pos = [ entPos[0] - d * sina, - entPos[1] - d * cosa]; - access = gameState.ai.accessibility.getAccessValue(pos); - if (access > 1) - break; - } - } - ent.setMetadata(PlayerID, "access", access); - } - return access; -}; - -/** Sea access always cached as it never changes */ -m.getSeaAccess = function(gameState, ent) -{ - let sea = ent.getMetadata(PlayerID, "sea"); - if (!sea) - { - sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); - // Docks are sometimes not as expected - if (sea < 2 && ent.buildPlacementType() == "shore") - { - let entPos = ent.position(); - let cosa = Math.cos(ent.angle()); - let sina = Math.sin(ent.angle()); - for (let d = 3; d < 15; d += 3) - { - let pos = [ entPos[0] + d * sina, - entPos[1] + d * cosa]; - sea = gameState.ai.accessibility.getAccessValue(pos, true); - if (sea > 1) - break; - } - } - ent.setMetadata(PlayerID, "sea", sea); - } - return sea; -}; - -m.setSeaAccess = function(gameState, ent) -{ - m.getSeaAccess(gameState, ent); -}; - -/** Decide if we should try to capture (returns true) or destroy (return false) */ -m.allowCapture = function(gameState, ent, target) -{ - if (!target.isCapturable() || !ent.canCapture(target)) - return false; - if (target.isInvulnerable()) - return true; - // always try to recapture cp from an allied, except if it's decaying - if (gameState.isPlayerAlly(target.owner())) - return !target.decaying(); - - let antiCapture = target.defaultRegenRate(); - if (target.isGarrisonHolder() && target.garrisoned()) - antiCapture += target.garrisonRegenRate() * target.garrisoned().length; - if (target.decaying()) - antiCapture -= target.territoryDecayRate(); - - let capture; - let capturableTargets = gameState.ai.HQ.capturableTargets; - if (!capturableTargets.has(target.id())) - { - capture = ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); - } - else - { - let capturable = capturableTargets.get(target.id()); - if (!capturable.ents.has(ent.id())) - { - capturable.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturable.ents.add(ent.id()); - } - capture = capturable.strength; - } - capture *= 1 / (0.1 + 0.9*target.healthLevel()); - let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); - if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) - return capture > antiCapture + sumCapturePoints/50; - return capture > antiCapture + sumCapturePoints/80; -}; - -m.getAttackBonus = function(ent, target, type) -{ - let attackBonus = 1; - if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) - return attackBonus; - let bonuses = ent.get("Attack/" + type + "/Bonuses"); - for (let key in bonuses) - { - let bonus = bonuses[key]; - if (bonus.Civ && bonus.Civ !== target.civ()) - continue; - if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) - continue; - attackBonus *= bonus.Multiplier; - } - return attackBonus; -}; - -/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ -m.returnResources = function(gameState, ent) -{ - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) - return false; - - let resource = ent.resourceCarrying()[0].type; - - let closestDropsite; - let distmin = Math.min(); - let access = m.getLandAccess(gameState, ent); - let dropsiteCollection = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); - for (let dropsite of dropsiteCollection.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again - if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - closestDropsite = dropsite; - } - - if (!closestDropsite) - return false; - ent.returnResources(closestDropsite); - return true; -}; - -/** is supply full taking into account gatherers affected during this turn */ -m.IsSupplyFull = function(gameState, ent) -{ - return ent.isFull() === true || - ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); -}; - -/** - * Get the best base (in terms of distance and accessIndex) for an entity. - * It should be on the same accessIndex for structures. - * If nothing found, return the base[0] for units and undefined for structures. - * If exclude is given, we exclude the base with ID = exclude. - */ -m.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) -{ - let pos = ent.position(); - let accessIndex; - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - { - API3.warn("Petra error: entity without position, but not garrisoned"); - m.dumpEntity(ent); - return gameState.ai.HQ.baseManagers[0]; - } - pos = holder.position(); - accessIndex = m.getLandAccess(gameState, holder); - } - else - accessIndex = m.getLandAccess(gameState, ent); - - let distmin = Math.min(); - let dist; - let bestbase; - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) - continue; - if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) - continue; - if (ent.hasClass("Structure") && base.accessIndex != accessIndex) - continue; - if (base.anchor && base.anchor.position()) - dist = API3.SquareVectorDistance(base.anchor.position(), pos); - else - { - let found = false; - for (let structure of base.buildings.values()) - { - if (!structure.position()) - continue; - dist = API3.SquareVectorDistance(structure.position(), pos); - found = true; - break; - } - if (!found) - continue; - } - if (base.accessIndex != accessIndex) - dist += 50000000; - if (!base.anchor) - dist += 50000000; - if (dist > distmin) - continue; - distmin = dist; - bestbase = base; - } - if (!bestbase && !ent.hasClass("Structure")) - bestbase = gameState.ai.HQ.baseManagers[0]; - return bestbase; -}; - -m.getHolder = function(gameState, ent) -{ - for (let holder of gameState.getEntities().values()) - { - if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) - return holder; - } - return undefined; -}; - -/** return the template of the built foundation if a foundation, otherwise return the entity itself */ -m.getBuiltEntity = function(gameState, ent) -{ - if (ent.foundationProgress() !== undefined) - return gameState.getBuiltTemplate(ent.templateName()); - - return ent; -}; - -/** - * return true if it is not worth finishing this building (it would surely decay) - * TODO implement the other conditions - */ -m.isNotWorthBuilding = function(gameState, ent) -{ - if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) - { - let buildTerritories = ent.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) - return true; - } - return false; -}; - -/** - * Check if the straight line between the two positions crosses an enemy territory - */ -m.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) -{ - let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; - let stepx = (pos2[0] - pos1[0]) / n; - let stepy = (pos2[1] - pos1[1]) / n; - for (let i = 1; i < n; ++i) - { - let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; - let owner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (owner && gameState.isPlayerEnemy(owner)) - return true; - } - return false; -}; - -m.gatherTreasure = function(gameState, ent, water = false) -{ - if (!gameState.ai.HQ.treasures.hasEntities()) - return false; - if (!ent || !ent.position()) - return false; - let rates = ent.resourceGatherRates(); - if (!rates || !rates.treasure || rates.treasure <= 0) - return false; - let treasureFound; - let distmin = Math.min(); - let access = water ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - for (let treasure of gameState.ai.HQ.treasures.values()) - { - if (m.IsSupplyFull(gameState, treasure)) - continue; - // let some time for the previous gatherer to reach the treasure before trying again - let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); - if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) - continue; - if (!water && access != m.getLandAccess(gameState, treasure)) - continue; - if (water && access != m.getSeaAccess(gameState, treasure)) - continue; - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - continue; - let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); - if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit - continue; - if (dist > distmin) - continue; - distmin = dist; - treasureFound = treasure; - } - if (!treasureFound) - return false; - treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); - ent.gather(treasureFound); - gameState.ai.HQ.AddTCGatherer(treasureFound.id()); - ent.setMetadata(PlayerID, "supply", treasureFound.id()); - return true; -}; - -m.dumpEntity = function(ent) -{ - if (!ent) - return; - API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + - " state " + ent.unitAIState()); - API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + - " subrole " + ent.getMetadata(PlayerID, "subrole")); - API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + - " foundationProgress " + ent.foundationProgress()); - API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + - " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + - " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); - API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + - " gather-type " + ent.getMetadata(PlayerID, "gather-type") + - " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + - " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/garrisonManager.js b/install/petraBased/petra-unitary/garrisonManager.js deleted file mode 100644 index c9ec57c..0000000 --- a/install/petraBased/petra-unitary/garrisonManager.js +++ /dev/null @@ -1,373 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the garrisonHolders - * When a unit is ordered to garrison, it must be done through this.garrison() function so that - * an object in this.holders is created. This object contains an array with the entities - * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). - * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) - */ - -m.GarrisonManager = function(Config) -{ - this.Config = Config; - this.holders = new Map(); - this.decayingStructures = new Map(); -}; - -m.GarrisonManager.prototype.update = function(gameState, events) -{ - // First check for possible upgrade of a structure - for (let evt of events.EntityRenamed) - { - for (let id of this.holders.keys()) - { - if (id != evt.entity) - continue; - let data = this.holders.get(id); - let newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) - { - this.holders.delete(id); - this.holders.set(evt.newentity, data); - } - else - { - for (let entId of data.list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - continue; - this.leaveGarrison(ent); - ent.stopMoving(); - } - this.holders.delete(id); - } - } - - for (let id of this.decayingStructures.keys()) - { - if (id !== evt.entity) - continue; - this.decayingStructures.delete(id); - if (this.decayingStructures.has(evt.newentity)) - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) - continue; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(evt.newentity, gmin); - } - } - - for (let [id, data] of this.holders.entries()) - { - let list = data.list; - let holder = gameState.getEntityById(id); - if (!holder || !gameState.isPlayerAlly(holder.owner())) - { - // this holder was certainly destroyed or captured. Let's remove it - for (let entId of list) - { - let ent = gameState.getEntityById(entId); - if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) - 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 (let evt of events.EntityRenamed) - if (evt.entity === list[j]) - list[j] = evt.newentity; - - let ent = gameState.getEntityById(list[j]); - if (!ent) // unit must have been killed while garrisoning - list.splice(j--, 1); - else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned - { - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (ent.unitAIOrderData().some(order => order.target && order.target == id)) - continue; - if (ent.getMetadata(PlayerID, "garrisonHolder") == id) - { - // The garrison order must have failed - this.leaveGarrison(ent); - list.splice(j--, 1); - } - else - { - if (gameState.ai.Config.debug > 0) - { - API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + - ") is expected to garrison in " + id + " (" + holder.genericName() + - "), but has no such garrison order " + uneval(ent.unitAIOrderData())); - m.dumpEntity(ent); - } - list.splice(j--, 1); - } - } - - } - - if (!holder.position()) // could happen with siege unit inside a ship - continue; - - if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) - { - let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; - let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; - for (let 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; - let dist = API3.SquareVectorDistance(ent.position(), holder.position()); - if (dist > range*range) - continue; - if (ent.hasClass("Structure")) - around.defenseStructure = true; - else if (m.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 (let entId of holder.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) - holder.unload(entId); - } - for (let j = 0; j < list.length; ++j) - { - let 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.numberOfGarrisonedUnits(holder) === 0) - this.holders.delete(id); - else - holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); - } - } - - // Warning new garrison orders (as in the following lines) should be done after having updated the holders - // (or TODO we should add a test that the garrison order is from a previous turn when updating) - for (let [id, gmin] of this.decayingStructures.entries()) - { - let ent = gameState.getEntityById(id); - if (!ent || ent.owner() !== PlayerID) - this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedUnits(ent) < gmin) - gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); - } -}; - -/** TODO should add the units garrisoned inside garrisoned units */ -m.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) -{ - if (!this.holders.has(holder.id())) - return holder.garrisoned().length; - - return holder.garrisoned().length + this.holders.get(holder.id()).list.length; -}; - -m.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 */ -m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) -{ - if (this.numberOfGarrisonedUnits(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")); - } - - if (ent.getMetadata(PlayerID, "plan") !== undefined) - ent.setMetadata(PlayerID, "plan", -2); - else - ent.setMetadata(PlayerID, "plan", -3); - ent.setMetadata(PlayerID, "subrole", "garrisoning"); - ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); - ent.setMetadata(PlayerID, "garrisonType", type); - ent.garrison(holder); -}; - -/** - This is the end of the pre-garrison state, either because the entity is really garrisoned - or because it has changed its order (i.e. because the garrisonHolder was destroyed) - 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 - */ -m.GarrisonManager.prototype.leaveGarrison = function(ent) -{ - ent.setMetadata(PlayerID, "subrole", undefined); - if (ent.getMetadata(PlayerID, "plan") === -2) - ent.setMetadata(PlayerID, "plan", -1); - else - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "garrisonHolder", undefined); -}; - -/** Cancel a pre-garrison state */ -m.GarrisonManager.prototype.cancelGarrison = function(ent) -{ - ent.stopMoving(); - this.leaveGarrison(ent); - let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); - if (!holderId || !this.holders.has(holderId)) - return; - let list = this.holders.get(holderId).list; - let index = list.indexOf(ent.id()); - if (index !== -1) - list.splice(index, 1); -}; - -m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) -{ - switch (ent.getMetadata(PlayerID, "garrisonType")) - { - case 'force': // force the ungarrisoning - return false; - case 'trade': // trader garrisoned in ship - return true; - case 'protection': // hurt unit for healing or infantry for defense - if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) - return true; - let capture = ent.capturePoints(); - if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) - return true; - if (MatchesClassList(ent.classes(), 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") || m.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above - if (m.isSiegeUnit(ent)) - return around.meleeSiege; - return holder.buffHeal() && ent.needsHeal(); - case 'decay': - return this.decayingStructures.has(holder.id()); - case '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; - API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + - " for " + ent.genericName() + " id " + ent.id() + - " inside " + holder.genericName() + " id " + holder.id()); - ent.setMetadata(PlayerID, "garrisonType", "protection"); - return true; - } -}; - -/** Add this holder in the list managed by the garrisonManager */ -m.GarrisonManager.prototype.registerHolder = function(gameState, holder) -{ - if (this.holders.has(holder.id())) // already registered - return; - this.holders.set(holder.id(), { "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) - */ -m.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) -{ - if (this.decayingStructures.has(entId)) - return true; - let ent = gameState.getEntityById(entId); - if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) - return false; - if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) - return false; - let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); - this.decayingStructures.set(entId, gmin); - return true; -}; - -m.GarrisonManager.prototype.removeDecayingStructure = function(entId) -{ - if (!this.decayingStructures.has(entId)) - return; - this.decayingStructures.delete(entId); -}; - -m.GarrisonManager.prototype.Serialize = function() -{ - return { "holders": this.holders, "decayingStructures": this.decayingStructures }; -}; - -m.GarrisonManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/headquarters.js b/install/petraBased/petra-unitary/headquarters.js deleted file mode 100644 index 407e77b..0000000 --- a/install/petraBased/petra-unitary/headquarters.js +++ /dev/null @@ -1,2900 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Headquarters - * Deal with high level logic for the AI. Most of the interesting stuff gets done here. - * Some tasks: - * -defining RESS needs - * -BO decisions. - * > training workers - * > building stuff (though we'll send that to bases) - * -picking strategy (specific manager?) - * -diplomacy -> diplomacyManager - * -planning attacks -> attackManager - * -picking new CC locations. - */ - -m.HQ = function(Config) -{ - this.Config = Config; - this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i - - // Cache various quantities. - this.turnCache = {}; - this.lastFailedGather = {}; - - this.firstBaseConfig = false; - this.currentBase = 0; // Only one base (from baseManager) is run every turn. - - // Workers configuration - this.targetNumWorkers = this.Config.Economy.targetNumWorkers; - this.supportRatio = this.Config.Economy.supportRatio; - - this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime - this.towerStartTime = 0; // stone defense towers, will start as soon as available - this.towerLapseTime = this.Config.Military.towerLapseTime; - this.fortressStartTime = 0; // will start as soon as available - this.fortressLapseTime = this.Config.Military.fortressLapseTime; - this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); - this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); - - this.baseManagers = []; - this.attackManager = new m.AttackManager(this.Config); - this.buildManager = new m.BuildManager(); - this.defenseManager = new m.DefenseManager(this.Config); - this.tradeManager = new m.TradeManager(this.Config); - this.navalManager = new m.NavalManager(this.Config); - this.researchManager = new m.ResearchManager(this.Config); - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.garrisonManager = new m.GarrisonManager(this.Config); - this.victoryManager = new m.VictoryManager(this.Config); - - this.capturableTargets = new Map(); - this.capturableTargetsTime = 0; -}; - -/** More initialisation for stuff that needs the gameState */ -m.HQ.prototype.init = function(gameState, queues) -{ - this.territoryMap = m.createTerritoryMap(gameState); - // initialize base map. Each pixel is a base ID, or 0 if not or not accessible - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - // create borderMap: flag cells on the border of the map - // then this map will be completed with our frontier in updateTerritories - this.borderMap = m.createBorderMap(gameState); - // list of allowed regions - this.landRegions = {}; - // try to determine if we have a water map - this.navalMap = false; - this.navalRegions = {}; - - this.treasures = gameState.getEntities().filter(ent => { - let type = ent.resourceSupplyType(); - return type && type.generic == "treasure"; - }); - this.treasures.registerUpdates(); - this.currentPhase = gameState.currentPhase(); - this.decayingStructures = new Set(); -}; - -/** - * initialization needed after deserialization (only called when deserialization) - */ -m.HQ.prototype.postinit = function(gameState) -{ - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("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 - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - let base = this.getBaseByID(baseID); - base.assignResourceToDropsite(gameState, ent); - } - - this.updateTerritories(gameState); -}; - -/** - * 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 - * allowedType: undefined => new base with an anchor - * "unconstructed" => new base with a foundation anchor - * "captured" => captured base with an anchor - * "anchorless" => anchorless base, currently with dock - */ -m.HQ.prototype.createBase = function(gameState, ent, type) -{ - let access = m.getLandAccess(gameState, ent); - let newbase; - for (let base of this.baseManagers) - { - if (base.accessIndex != access) - continue; - if (type != "anchorless" && base.anchor) - continue; - if (type != "anchorless") - { - // TODO we keep the fisrt 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) - continue; - newbase = base; - if (newbase.anchor) - break; - } - } - - if (this.Config.debug > 0) - { - API3.warn(" ----------------------------------------------------------"); - API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); - API3.warn(" 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 m.BaseManager(gameState, this.Config); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type != "anchorless") - newbase.setAnchor(gameState, ent); - else - newbase.setAnchorlessEntity(gameState, ent); - - return newbase; -}; - -/** - * returns the sea index linking regions 1 and region 2 (supposed to be different land region) - * otherwise return undefined - * for the moment, only the case land-sea-land is supported - */ -m.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) -{ - let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); - if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") - return path[1]; - - if (this.Config.debug > 1) - { - API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); - API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); - API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); - } - return undefined; -}; - -/** TODO check if the new anchorless bases should be added to addBase */ -m.HQ.prototype.checkEvents = function(gameState, events) -{ - let addBase = false; - - this.buildManager.checkEvents(gameState, events); - - if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) - this.updateTerritories(gameState); - - for (let evt of events.DiplomacyChanged) - { - if (evt.player != PlayerID && evt.otherPlayer != PlayerID) - continue; - // Reset the entities collections which depend on diplomacy - gameState.resetOnDiplomacyChanged(); - break; - } - - for (let evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && - evt.metadata[PlayerID].base) - { - let ent = evt.entityObj; - if (ent.owner() != PlayerID) - continue; - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - let base = this.getBaseByID(evt.metadata[PlayerID].base); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState, ent); - } - } - - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (let evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - let newbase = this.createBase(gameState, ent, "unconstructed"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - let newbase = this.createBase(gameState, ent, "anchorless"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } - - for (let evt of events.ConstructionFinished) - { - if (evt.newentity == evt.entity) // repaired building - continue; - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID) - continue; - if (ent.hasClass("BarterMarket") && this.maxFields) - this.maxFields = false; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.from == PlayerID) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState, ent); - } - - if (evt.to != PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent) - continue; - if (ent.hasClass("Unit")) - { - m.getBestBase(gameState, ent).assignEntity(gameState, ent); - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - if (ent.hasClass("Trader")) - { - ent.setMetadata(PlayerID, "role", "trader"); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", "worker"); - ent.setMetadata(PlayerID, "subrole", "idle"); - } - if (ent.hasClass("Ship")) - m.setSeaAccess(gameState, ent); - if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, "unconstructed"); - else - { - newbase = this.createBase(gameState, ent, "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, "anchorless"); - else - base = m.getBestBase(gameState, ent) || this.baseManagers[0]; - base.assignEntity(gameState, ent); - if (ent.decaying()) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - } - } - - // deal with the different rally points of training units: the rally point is set when the training starts - // for the time being, only autogarrison is used - - for (let evt of events.TrainingStarted) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) - continue; - let metadata = ent._entity.trainingQueue[0].metadata; - if (metadata && metadata.garrisonType) - ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison - else - ent.unsetRallyPoint(); - } - - for (let evt of events.TrainingFinished) - { - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.isOwn(PlayerID)) - continue; - - if (!ent.position()) - { - // we are autogarrisoned, check that the holder is registered in the garrisonManager - let holderId = ent.unitAIOrderData()[0].target; - let holder = gameState.getEntityById(holderId); - if (holder) - this.garrisonManager.registerHolder(gameState, holder); - } - else if (ent.getMetadata(PlayerID, "garrisonType")) - { - // we were supposed to be autogarrisoned, but this has failed (may-be full) - ent.setMetadata(PlayerID, "garrisonType", undefined); - } - - // Check if this unit is no more needed in its attack plan - // (happen when the training ends after the attack is started or aborted) - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan !== undefined && plan >= 0) - { - let attack = this.attackManager.getPlan(plan); - if (!attack || attack.state != "unexecuted") - ent.setMetadata(PlayerID, "plan", -1); - } - // Assign it immediately to something useful to do - if (ent.getMetadata(PlayerID, "role") == "worker") - { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) - { - base = m.getBestBase(gameState, ent); - base.assignEntity(gameState, ent); - } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); - } - else if (ent.resourceSupplyType() && ent.position()) - { - let type = ent.resourceSupplyType(); - if (!type.generic) - continue; - let dropsites = gameState.getOwnDropsites(type.generic); - let pos = ent.position(); - let access = m.getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (let dropsite of dropsites.values()) - { - if (!dropsite.position() || m.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); - } - if (goal) - ent.moveToRange(goal[0], goal[1]); - } - } - } - - for (let evt of events.TerritoryDecayChanged) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) - continue; - if (evt.to) - { - if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) - continue; - if (!this.decayingStructures.has(evt.entity)) - this.decayingStructures.add(evt.entity); - } - else if (ent.isGarrisonHolder()) - this.garrisonManager.removeDecayingStructure(evt.entity); - } - - if (addBase) - { - if (!this.firstBaseConfig) - { - // This is our first base, let us configure our starting resources - this.configFirstBase(gameState); - } - else - { - // Let us hope this new base will fix our possible resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } - } - - // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) - if (this.Config.difficulty < 2) - return; - for (let entId of this.decayingStructures) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.decaying() && ent.isOwn(PlayerID)) - { - let capture = ent.capturePoints(); - if (!capture) - continue; - let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); - if (captureRatio < 0.50) - continue; - let decayToGaia = true; - for (let i = 1; i < capture.length; ++i) - { - if (gameState.isPlayerAlly(i) || !capture[i]) - continue; - decayToGaia = false; - break; - } - if (decayToGaia) - continue; - let ratioMax = 0.70 + randFloat(0., 0.1); - for (let evt of events.Attacked) - { - if (ent.id() != evt.target) - continue; - ratioMax = 0.85 + randFloat(0., 0.1); - break; - } - if (captureRatio > ratioMax) - continue; - ent.destroy(); - } - this.decayingStructures.delete(entId); - } -}; - -/** Ensure that all requirements are met when phasing up*/ -m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) -{ - if (gameState.getNumberOfPhases() == this.currentPhase) - return; - - let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); - let plan; - let queue; - for (let entityReq of requirements) - { - // Village requirements are met elsewhere by constructing more houses - if (entityReq.class == "Village" || entityReq.class == "NotField") - continue; - if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) - continue; - switch (entityReq.class) - { - case "Town": - if (!queues.economicBuilding.hasQueuedUnits() && - !queues.militaryBuilding.hasQueuedUnits() && - !queues.defenseBuilding.hasQueuedUnits()) - { - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_market")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_market", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_temple")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "phaseUp": true }); - queue = "economicBuilding"; - break; - } - if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && - this.canBuild(gameState, "structures/{civ}_blacksmith")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "phaseUp": true }); - queue = "militaryBuilding"; - break; - } - if (this.canBuild(gameState, "structures/{civ}_defense_tower")) - { - plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower", { "phaseUp": true }); - queue = "defenseBuilding"; - break; - } - } - break; - default: - // All classes not dealt with inside vanilla game. - // We put them for the time being on the economic queue, except if wonder - queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; - if (!queues[queue].hasQueuedUnits()) - { - let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); - if (structure && this.canBuild(gameState, structure)) - plan = new m.ConstructionPlan(gameState, structure, { "phaseUp": true }); - } - } - - 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); - return; - } - } -}; - -/** Called by any "phase" research plan once it's started */ -m.HQ.prototype.OnPhaseUp = function(gameState, phase) -{ -}; - -/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ -m.HQ.prototype.trainMoreWorkers = function(gameState, queues) -{ - // default template - let requirementsDef = [ ["costsResource", 1, "food"] ]; - let classesDef = ["Support", "Worker"]; - let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); - - // counting the workers that aren't part of a plan - let numberOfWorkers = 0; // all workers - let numberOfSupports = 0; // only support workers (i.e. non fighting) - gameState.getOwnUnits().forEach(ent => { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) - { - ++numberOfWorkers; - if (ent.hasClass("Support")) - ++numberOfSupports; - } - }); - let numberInTraining = 0; - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - numberInTraining += item.count; - if (item.metadata && item.metadata.role && item.metadata.role == "worker" && - item.metadata.plan === undefined) - { - numberOfWorkers += item.count; - if (item.metadata.support) - numberOfSupports += item.count; - } - } - }); - - // Anticipate the optimal batch size when this queue will start - // and adapt the batch size of the first and second queued workers to the present population - // to ease a possible recovery if our population was drastically reduced by an attack - // (need to go up to second queued as it is accounted in queueManager) - let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); - if (queues.villager.plans[0]) - { - queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); - if (queues.villager.plans[1]) - queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); - } - if (queues.citizenSoldier.plans[0]) - { - queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); - if (queues.citizenSoldier.plans[1]) - queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); - } - - let numberOfQueuedSupports = queues.villager.countQueuedUnits(); - let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); - let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; - let numberTotal = numberOfWorkers + numberQueued; - - if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) - return; - if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && - this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) - return; - if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) - return; - - // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers - // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. - // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) - // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers - // and gives a ratio alfa at startup. - - let supportRatio = this.supportRatio; - let alpha = 0.85; - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field"))) - supportRatio = Math.min(this.supportRatio, 0.1); - if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) - alpha = 0.7; - if (gameState.isCeasefireActive()) - alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; - let supportMax = supportRatio * this.targetNumWorkers; - let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); - - let template; - if (!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] ]; - - let classes = ["CitizenSoldier", "Infantry"]; - // We want at least 33% ranged and 33% melee - classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); - - template = this.findBestTrainableUnit(gameState, classes, requirements); - } - - // If the template variable is empty, the default unit (Support unit) will be used - // base "0" means automatic choice of base - if (!template && templateDef) - queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); - else if (template) - queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); -}; - -/** picks the best template based on parameters and classes */ -m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) -{ - let units; - if (classes.indexOf("Hero") != -1) - units = gameState.findTrainableUnits(classes, []); - else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it - units = gameState.findTrainableUnits(classes, ["SiegeTower"]); - else // We do not want hero when not explicitely specified - units = gameState.findTrainableUnits(classes, ["Hero"]); - - if (!units.length) - return undefined; - - let parameters = requirements.slice(); - let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory - let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources - for (let type in remainingResources) - { - if (availableResources[type] > 800) - continue; - if (remainingResources[type] > 800) - continue; - let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; - let toAdd = true; - for (let param of parameters) - { - if (param[0] != "costsResource" || param[2] != type) - continue; - param[1] = Math.min(param[1], costsResource); - toAdd = false; - break; - } - if (toAdd) - parameters.push(["costsResource", costsResource, type]); - } - - units.sort((a, b) => { - let aCost = 1 + a[1].costSum(); - let bCost = 1 + b[1].costSum(); - let aValue = 0.1; - let bValue = 0.1; - for (let param of parameters) - { - if (param[0] == "strength") - { - aValue += m.getMaxStrength(a[1]) * param[1]; - bValue += m.getMaxStrength(b[1]) * param[1]; - } - else if (param[0] == "siegeStrength") - { - aValue += m.getMaxStrength(a[1], "Structure") * param[1]; - bValue += m.getMaxStrength(b[1], "Structure") * param[1]; - } - else if (param[0] == "speed") - { - aValue += a[1].walkSpeed() * param[1]; - bValue += b[1].walkSpeed() * param[1]; - } - else if (param[0] == "costsResource") - { - // requires a third parameter which is the resource - if (a[1].cost()[param[2]]) - aValue *= param[1]; - if (b[1].cost()[param[2]]) - bValue *= param[1]; - } - else if (param[0] == "canGather") - { - // checking against wood, could be anything else really. - if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) - aValue *= param[1]; - if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) - bValue *= param[1]; - } - else - API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); - } - return -aValue/aCost + bValue/bCost; - }); - return units[0][0]; -}; - -/** - * returns an entity collection of workers through BaseManager.pickBuilders - * TODO: when same accessIndex, sort by distance - */ -m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) -{ - let accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - // sorting bases by whether they are on the same accessindex or not. - let baseBest = this.baseManagers.slice().sort((a, b) => { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); - - let needed = number; - let workers = new API3.EntityCollection(gameState.sharedScript); - for (let base of baseBest) - { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) - break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; -}; - -m.HQ.prototype.getTotalResourceLevel = function(gameState) -{ - let total = {}; - for (let res of Resources.GetCodes()) - total[res] = 0; - for (let base of this.baseManagers) - for (let res in total) - total[res] += base.getResourceLevel(gameState, res); - - return total; -}; - -/** - * Returns the current gather rate - * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. - */ -m.HQ.prototype.GetCurrentGatherRates = function(gameState) -{ - if (!this.turnCache.currentRates) - { - let currentRates = {}; - for (let res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); - - for (let res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; -}; - -/** - * Returns the wanted gather rate. - */ -m.HQ.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. - */ -m.HQ.prototype.pickMostNeededResources = function(gameState) -{ - let wantedRates = this.GetWantedGatherRates(gameState); - let currentRates = this.GetCurrentGatherRates(gameState); - - let needed = []; - for (let res in wantedRates) - 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 Centre - * Whose primary function would be to reach new resources of type "resource". - */ -m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) -{ - // This builds a map. The procedure is fairly simple. It adds the resource maps - // (which are dynamically updated and are made so that they will facilitate DP placement) - // Then look for a good spot. - - Engine.ProfileStart("findEconomicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); - let ccList = []; - for (let cc of ccEnts.values()) - ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); - let dpList = []; - for (let 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; - let nbShips = this.navalManager.transportShips.length; - if (proximity) // this is our first base - { - // if our first base, ensure room around - radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); - // scale is the typical scale at which we want to find a location for our first base - // look for bigger scale if we start from a ship (access < 2) or from a small island - let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; - proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); - if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) - scale = 400 * 400; - } - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). - let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; - let nearbyRejected = Math.square(120); // Reject if too near from any cc - let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc - let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc - let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc - let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc - let 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // We require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - if (proxyAccess && nbShips == 0 && proxyAccess != index) - continue; - - let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps - // Checking distance to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // 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 + API3.SquareVectorDistance(proximity, pos) / scale; - else - { - let minDist = Math.min(); - let accessible = false; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < nearbyRejected) - { - norm = 0; - break; - } - if (!cc.ally) - continue; - if (dist < nearbyAllyRejected) - { - norm = 0; - break; - } - if (dist < nearbyAllyDisfavored) - norm *= 0.5; - - if (dist < minDist) - minDist = dist; - accessible = accessible || index == m.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; - } - - // Not near any of our dropsite, except for oversea docks - oversea = !accessible && dpList.some(dp => m.getLandAccess(gameState, dp.ent) == index); - if (!oversea) - { - for (let dp of dpList) - { - let dist = API3.SquareVectorDistance(dp.pos, pos); - if (dist < 3600) - { - norm = 0; - break; - } - else if (dist < 6400) - norm *= 0.5; - } - } - if (norm == 0) - continue; - } - - if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map - norm *= 0.5; - - let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j]; - for (let res in gameState.sharedScript.resourceMaps) - if (res != "food") - val += gameState.sharedScript.ccResourceMaps[res].map[j]; - val *= norm; - - // If 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) - API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); - // not good enough. - if (bestVal < cut) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new Civil Centre - * Whose primary function would be to assure territorial continuity with our allies - */ -m.HQ.prototype.findStrategicCCLocation = function(gameState, template) -{ - // This builds a map. The procedure is fairly simple. - // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC - // with the constraints that all CC have dist > 200 and at least one have dist < 400 - // This needs at least 2 CC. Otherwise, go back to economic CC. - - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - let ccList = []; - let numAllyCC = 0; - for (let cc of ccEnts.values()) - { - let ally = gameState.isPlayerAlly(cc.owner()); - ccList.push({ "pos": cc.position(), "ally": ally }); - if (ally) - ++numAllyCC; - } - if (numAllyCC < 2) - return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); - - Engine.ProfileStart("findStrategicCCLocation"); - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestVal; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let currentVal, delta; - let distcc0, distcc1, distcc2; - let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 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 - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - // we require that it is accessible - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - - // checking distances to other cc - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - let minDist = Math.min(); - distcc0 = undefined; - - for (let cc of ccList) - { - let dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < 14000) // Reject if too near from any cc - { - minDist = 0; - break; - } - if (!cc.ally) - continue; - if (dist < 62000) // Reject if quite near from ally cc - { - minDist = 0; - break; - } - if (dist < minDist) - minDist = dist; - - if (!distcc0 || dist < distcc0) - { - distcc2 = distcc1; - distcc1 = distcc0; - distcc0 = dist; - } - else if (!distcc1 || dist < distcc1) - { - distcc2 = distcc1; - distcc1 = dist; - } - else if (!distcc2 || dist < distcc2) - distcc2 = dist; - } - if (minDist < 1 || minDist > 170000 && !this.navalMap) - continue; - - delta = Math.sqrt(distcc0) - favoredDistance; - currentVal = delta*delta; - delta = Math.sqrt(distcc1) - favoredDistance; - currentVal += delta*delta; - if (distcc2) - { - delta = Math.sqrt(distcc2) - favoredDistance; - currentVal += delta*delta; - } - // disfavor border of the map - if (this.borderMap.map[j] & m.fullBorder_Mask) - currentVal += 10000; - - if (bestVal !== undefined && currentVal > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = currentVal; - bestIdx = i; - } - - if (this.Config.debug > 1) - API3.warn("We've found a strategic base with bestVal = " + bestVal); - - Engine.ProfileStop(); - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Define a minimal number of wanted ships in the seas reaching this new base - let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex == indexIdx) - continue; - let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); - if (sea !== undefined) - this.navalManager.setMinimalTransportShips(gameState, sea, 1); - } - - return [x, z]; -}; - -/** - * Returns the best position to build a new market: if the allies already have a market, build it as far as possible - * from it, although not in our border to be able to defend it easily. If no allied market, our second market will - * follow the same logic. - * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance - * for performance reasons. - */ -m.HQ.prototype.findMarketLocation = function(gameState, template) -{ - let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); - if (!markets.length) - markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); - - if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let bestDistSq; - let bestGainMult; - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - let isNavalMarket = template.hasClass("NavalMarket"); - - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - // do not try on the narrow border of our territory - if (this.borderMap.map[j] & m.narrowFrontier_Mask) - continue; - if (this.basesMap.map[j] == 0) // only in our territory - continue; - // with enough room around to build the market - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - let index = gameState.ai.accessibility.landPassMap[i]; - if (!this.landRegions[index]) - continue; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other markets - let maxVal = 0; - let maxDistSq; - let maxGainMult; - let gainMultiplier; - for (let market of markets) - { - if (isNavalMarket && market.hasClass("NavalMarket")) - { - if (m.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) - continue; - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - } - else if (m.getLandAccess(gameState, market) == index && - !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else - continue; - if (!gainMultiplier) - continue; - let distSq = API3.SquareVectorDistance(market.position(), pos); - if (gainMultiplier * distSq > maxVal) - { - maxVal = gainMultiplier * distSq; - maxDistSq = distSq; - maxGainMult = gainMultiplier; - } - } - if (maxVal == 0) - continue; - if (bestVal !== undefined && maxVal < bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = maxVal; - bestDistSq = maxDistSq; - bestGainMult = maxGainMult; - bestIdx = i; - bestJdx = j; - } - - if (this.Config.debug > 1) - API3.warn("We found a market position with bestVal = " + bestVal); - - if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan - return [-1, -1, -1, 0]; - let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); - if (this.Config.debug > 1) - API3.warn("this would give a trading gain of " + expectedGain); - // do not keep it if gain is too small, except if this is our first BarterMarket - let idx; - if (expectedGain < this.tradeManager.minimalGain) - { - if (template.hasClass("BarterMarket") && - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - idx = -1; // needed by queueplanBuilding manager to keep that market - else - return false; - } - else - idx = this.basesMap.map[bestJdx]; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let 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 - */ -m.HQ.prototype.findDefensiveLocation = function(gameState, template) -{ - // We take the point in our territory which is the nearest to any enemy cc - // but requiring a minimal distance with our other defensive structures - // and not in range of any enemy defensive structure to avoid building under fire. - - let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); - let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals - { - enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) - enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). - filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); - if (!enemyStructures.hasEntities()) - return undefined; - } - enemyStructures = enemyStructures.toEntityArray(); - - let wonderMode = gameState.getVictoryConditions().has("wonder"); - let wonderDistmin; - let wonders; - if (wonderMode) - { - wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); - wonderMode = wonders.length != 0; - if (wonderMode) - wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); - } - - // obstruction map - let obstructions = m.createObstructionMap(gameState, 0, template); - let halfSize = 0; - if (template.get("Footprint/Square")) - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - else if (template.get("Footprint/Circle")) - halfSize = +template.get("Footprint/Circle/@radius"); - - let bestIdx; - let bestJdx; - let bestVal; - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - - let isTower = template.hasClass("Tower"); - let isFortress = template.hasClass("Fortress"); - let radius; - if (isFortress) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (!wonderMode) - { - // do not try if well inside or outside territory - if (!(this.borderMap.map[j] & m.fullFrontier_Mask)) - continue; - if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower) - continue; - } - if (this.basesMap.map[j] == 0) // inaccessible cell - continue; - // with enough room around to build the cc - let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - // checking distances to other structures - let minDist = Math.min(); - - let dista = 0; - if (wonderMode) - { - dista = API3.SquareVectorDistance(wonders[0].position(), pos); - if (dista < wonderDistmin) - continue; - dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder - } - - for (let str of enemyStructures) - { - if (str.foundationProgress() !== undefined) - continue; - let strPos = str.position(); - if (!strPos) - continue; - let dist = API3.SquareVectorDistance(strPos, pos); - if (dist < 6400) // TODO check on true attack range instead of this 80*80 - { - minDist = -1; - break; - } - if (str.hasClass("CivCentre") && dist + dista < minDist) - minDist = dist + dista; - } - if (minDist < 0) - continue; - - let cutDist = 900; // 30*30 TODO maybe increase it - for (let str of ownStructures) - { - let strPos = str.position(); - if (!strPos) - continue; - if (API3.SquareVectorDistance(strPos, pos) < cutDist) - { - minDist = -1; - break; - } - } - if (minDist < 0 || minDist == Math.min()) - continue; - if (bestVal !== undefined && minDist > bestVal) - continue; - if (this.isDangerousLocation(gameState, pos, halfSize)) - continue; - bestVal = minDist; - bestIdx = i; - bestJdx = j; - } - - if (bestVal === undefined) - return undefined; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.basesMap.map[bestJdx]]; -}; - -m.HQ.prototype.buildTemple = function(gameState, queues) -{ - // at least one market (which have the same queue) should be build before any temple - if (queues.economicBuilding.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || - !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - // Try to build a temple earlier if in regicide to recruit healer guards - if (this.currentPhase < 3 && !gameState.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 m.ConstructionPlan(gameState, templateName)); -}; - -m.HQ.prototype.buildMarket = function(gameState, queues) -{ - if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_market")) - return; - - if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) - { - if (!queues.economicBuilding.paused) - { - // Put available resources in this market - let queueManager = gameState.ai.queueManager; - let cost = queues.economicBuilding.plans[0].getCost(); - queueManager.setAccounts(gameState, cost, "economicBuilding"); - if (!queueManager.canAfford("economicBuilding", cost)) - { - for (let q in queueManager.queues) - { - if (q == "economicBuilding") - continue; - queueManager.transferAccounts(cost, q, "economicBuilding"); - if (queueManager.canAfford("economicBuilding", cost)) - break; - } - } - } - return; - } - - gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -/** Build a farmstead */ -m.HQ.prototype.buildFarmstead = function(gameState, queues) -{ - // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) - if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) - return; - // Wait to have at least one dropsite and house before the farmstead - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) - return; - if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) - return; - if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) - return; - if (!this.canBuild(gameState, "structures/{civ}_farmstead")) - return; - - queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); -}; - -/** - * Try to build a wonder when required - * force = true when called from the victoryManager in case of Wonder victory condition. - */ -m.HQ.prototype.buildWonder = function(gameState, queues, force = false) -{ - if (queues.wonder && queues.wonder.hasQueuedUnits() || - gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || - !this.canBuild(gameState, "structures/{civ}_wonder")) - return; - - if (!force) - { - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_wonder")); - // Check that we have enough resources to start thinking to build a wonder - let cost = template.cost(); - let resources = gameState.getResources(); - let highLevel = 0; - let lowLevel = 0; - for (let res in cost) - { - if (resources[res] && resources[res] > 0.7 * cost[res]) - ++highLevel; - else if (!resources[res] || resources[res] < 0.3 * cost[res]) - ++lowLevel; - } - if (highLevel == 0 || lowLevel > 1) - return; - } - - queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); -}; - -/** Build a corral, and train animals there */ -m.HQ.prototype.manageCorral = function(gameState, queues) -{ - if (queues.corral.hasQueuedUnits()) - return; - - let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; - if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) && - nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) - { - if (this.canBuild(gameState, "structures/{civ}_corral")) - { - queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); - return; - } - if (!nCorral) - return; - } - - // And train some animals - let civ = gameState.getPlayerCiv(); - for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) - { - if (corral.foundationProgress() !== undefined) - continue; - let trainables = corral.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.isHuntable()) - continue; - let count = gameState.countEntitiesByType(trainable, true); - for (let item of corral.trainingQueue()) - count += item.count; - if (count > nCorral) - continue; - queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); - return; - } - } -}; - -/** - * build more houses if needed. - * kinda ugly, lots of special cases to both build enough houses but not tooo many… - */ -m.HQ.prototype.buildMoreHouses = function(gameState, queues) -{ - if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) || - gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return; - - let numPlanned = queues.house.length(); - if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) - { - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); - // change the starting condition according to the situation. - plan.goRequirement = "houseNeeded"; - queues.house.addPlan(plan); - } - - if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) - { - let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); - let houseTemplate = gameState.getTemplate(houseTemplateName); - - let needed = 0; - for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) - { - if (!houseTemplate.hasClass(entityReq.class)) - continue; - - let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; - if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to be less restrictive"); - this.buildManager.setBuildable(houseTemplateName); - this.requireHouses = true; - } - needed = Math.max(needed, entityReq.count - count); - } - - let houseQueue = queues.house.plans; - for (let i = 0; i < numPlanned; ++i) - if (houseQueue[i].isGo(gameState)) - --needed; - else if (needed > 0) - { - houseQueue[i].goRequirement = undefined; - --needed; - } - } - - if (this.requireHouses) - { - let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); - if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => - !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) - this.requireHouses = undefined; - } - - // When population limit too tight - // - if no room to build, try to improve with technology - // - otherwise increase temporarily the priority of houses - let house = gameState.applyCiv("structures/{civ}_house"); - let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; - let popBonus = gameState.getTemplate(house).getPopulationBonus(); - let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); - let priority; - if (freeSlots < 5) - { - if (this.buildManager.isUnbuildable(gameState, house)) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - priority = 2*this.Config.priorities.house; - } - else - priority = this.Config.priorities.house; - - if (priority && priority != gameState.ai.queueManager.getPriority("house")) - gameState.ai.queueManager.changePriority("house", priority); -}; - -/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ -m.HQ.prototype.checkBaseExpansion = function(gameState, queues) -{ - if (queues.civilCentre.hasQueuedUnits()) - return; - // First build one cc if all have been destroyed - if (this.numPotentialBases() == 0) - { - this.buildFirstBase(gameState); - return; - } - // Then expand if we have not enough room available for buildings - if (this.buildManager.numberMissingRoom(gameState) > 1) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because not enough room to build "); - this.buildNewBase(gameState, queues); - return; - } - // If we've already planned to phase up, wait a bit before trying to expand - if (this.phasing) - return; - // Finally expand if we have lots of units (threshold depending on the aggressivity value) - let activeBases = this.numActiveBases(); - let numUnits = gameState.getOwnUnits().length; - let numvar = 10 * (1 - this.Config.personality.aggressive); - if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) - { - if (this.Config.debug > 2) - API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); - this.buildNewBase(gameState, queues); - } -}; - -m.HQ.prototype.buildNewBase = function(gameState, queues, resource) -{ - if (this.numPotentialBases() >= m.maxBaseCount) - return false; - if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) - return false; - if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) - return false; - - let template; - // We require at least one of this civ civCentre as they may allow specific units or techs - let hasOwnCC = false; - for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) - { - if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}_civil_centre")) - continue; - hasOwnCC = true; - break; - } - if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else if (this.canBuild(gameState, "structures/{civ}_civil_centre")) - template = "structures/{civ}_civil_centre"; - else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) - template = "structures/{civ}_military_colony"; - else - return false; - - // base "-1" means new base. - if (this.Config.debug > 1) - API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); - queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); - return true; -}; - -/** Deals with building fortresses and towers along our border with enemies. */ -m.HQ.prototype.buildDefenses = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) - return; - - if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) - { - // try to build fortresses - if (this.canBuild(gameState, "structures/{civ}_fortress")) - { - let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; - if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && - numFortresses < this.numActiveBases() + 1 + this.extraFortresses && - numFortresses < Math.floor(gameState.getPopulation() / 25) && - gameState.getOwnFoundationsByClass("Fortress").length < 2) - { - this.fortressStartTime = gameState.ai.elapsedTime; - if (!numFortresses) - gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - return; - } - } - } - - if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) - { - let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers - let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime; - if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) - { - this.fortStartTime = gameState.ai.elapsedTime; - queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); - } - return; - } - - if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) - return; - - let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; - let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; - if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) && - numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && - numTowers < Math.floor(gameState.getPopulation() / 8) && - gameState.getOwnFoundationsByClass("DefenseTower").length < 3) - { - this.towerStartTime = gameState.ai.elapsedTime; - if (numTowers > 2 * this.numActiveBases() + 3) - gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); - plan.queueToReset = "defenseBuilding"; - queues.defenseBuilding.addPlan(plan); - } -}; - -m.HQ.prototype.buildBlacksmith = function(gameState, queues) -{ - if (this.getAccountedPopulation(gameState) < this.Config.Military.popForBlacksmith || - queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) - return; - // build a market before the blacksmith - if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) - return; - - if (this.canBuild(gameState, "structures/{civ}_blacksmith")) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); -}; - -/** - * Deals with constructing military buildings (barracks, stables…) - * They are mostly defined by Config.js. This is unreliable since changes could be done easily. - */ -m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) -{ - if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) - return; - - let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; - if (this.saveResources && numBarracks != 0) - return; - - let barracksTemplate = this.canBuild(gameState, "structures/{civ}_barracks") ? "structures/{civ}_barracks" : undefined; - - let rangeTemplate = this.canBuild(gameState, "structures/{civ}_range") ? "structures/{civ}_range" : undefined; - let numRanges = gameState.getOwnEntitiesByClass("Archery", true).length; - numBarracks -= numRanges; - - let stableTemplate = this.canBuild(gameState, "structures/{civ}_stables") ? "structures/{civ}_stables" : - this.canBuild(gameState, "structures/{civ}_stable") ? "structures/{civ}_stable" : undefined; - let numStables = gameState.getOwnEntitiesByClass("Stables", true).length; - - if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || - this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) - { - // first barracks/range and stables. - if (numBarracks + numRanges == 0) - { - let template = barracksTemplate || rangeTemplate; - if (template) - { - gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); - let plan = new m.ConstructionPlan(gameState, template, { "militaryBase": true }); - plan.queueToReset = "militaryBuilding"; - queues.militaryBuilding.addPlan(plan); - return; - } - } - if (numStables == 0 && stableTemplate) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Second range/barracks and stables - if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); - return; - } - - // Then 3rd barracks/range/stables if needed - if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) - { - let template = barracksTemplate || stableTemplate || rangeTemplate; - if (template) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); - return; - } - } - } - - if (this.saveResources) - return; - - if (this.currentPhase < 3) - return; - - if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true })); - return; - } - - if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities()) - { - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true })); - return; - } - - if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) - return; - - // Build advanced military buildings - let nAdvanced = 0; - for (let advanced of this.bAdvanced) - nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); - - if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) - { - for (let advanced of this.bAdvanced) - { - if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) - continue; - let template = gameState.getTemplate(advanced); - if (!template) - continue; - let civ = gameState.getPlayerCiv(); - if (template.hasDefensiveFire() || template.trainableEntities(civ)) - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true })); - else // not a military building, but still use this queue - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); - return; - } - } -}; - -/** - * Find base nearest to ennemies for military buildings. - */ -m.HQ.prototype.findBestBaseForMilitary = function(gameState) -{ - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); - let bestBase; - let enemyFound = false; - let distMin = Math.min(); - for (let cce of ccEnts) - { - if (gameState.isPlayerAlly(cce.owner())) - continue; - if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) - continue; - let access = m.getLandAccess(gameState, cce); - let isEnemy = gameState.isPlayerEnemy(cce.owner()); - for (let cc of ccEnts) - { - if (cc.owner() != PlayerID) - continue; - if (m.getLandAccess(gameState, cc) != access) - continue; - let dist = API3.SquareVectorDistance(cc.position(), cce.position()); - if (!enemyFound && isEnemy) - enemyFound = true; - else if (dist > distMin) - continue; - bestBase = cc.getMetadata(PlayerID, "base"); - distMin = dist; - } - } - return bestBase; -}; - -/** - * train with highest priority ranged infantry in the nearest civil centre from a given set of positions - * and garrison them there for defense - */ -m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) -{ - if (gameState.ai.queues.emergency.hasQueuedUnits()) - return false; - - let civ = gameState.getPlayerCiv(); - // find nearest base anchor - let distcut = 20000; - let nearestAnchor; - let distmin; - for (let pos of positions) - { - let access = gameState.ai.accessibility.getAccessValue(pos); - // check nearest base anchor - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (m.getLandAccess(gameState, base.anchor) != access) - continue; - if (!base.anchor.trainableEntities(civ)) // base still in construction - continue; - let queue = base.anchor._entity.trainingQueue; - if (queue) - { - let time = 0; - for (let item of queue) - if (item.progress > 0 || item.metadata && item.metadata.garrisonType) - time += item.timeRemaining; - if (time/1000 > 5) - continue; - } - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (nearestAnchor && dist > distmin) - continue; - distmin = dist; - nearestAnchor = base.anchor; - } - } - if (!nearestAnchor || distmin > distcut) - return false; - - // We will choose randomly ranged and melee units, except when garrisonHolder is full - // in which case we prefer melee units - let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); - if (nearestAnchor._entity.trainingQueue) - { - for (let item of nearestAnchor._entity.trainingQueue) - { - if (item.metadata && item.metadata.garrisonType) - numGarrisoned += item.count; - else if (!item.progress && (!item.metadata || !item.metadata.trainer)) - nearestAnchor.stopProduction(item.id); - } - } - let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && - nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); - let rangedWanted = randBool() && autogarrison; - - let total = gameState.getResources(); - let templateFound; - let trainables = nearestAnchor.trainableEntities(civ); - let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) - continue; - if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) - continue; - if (!total.canAfford(new API3.Resources(template.cost()))) - continue; - templateFound = [trainable, template]; - if (template.hasClass("Ranged") == rangedWanted) - break; - } - if (!templateFound) - return false; - - // Check first if we can afford it without touching the other accounts - // and if not, take some of other accounted resources - // TODO sort the queues to be substracted - let queueManager = gameState.ai.queueManager; - let cost = new API3.Resources(templateFound[1].cost()); - queueManager.setAccounts(gameState, cost, "emergency"); - if (!queueManager.canAfford("emergency", cost)) - { - for (let q in queueManager.queues) - { - if (q == "emergency") - continue; - queueManager.transferAccounts(cost, q, "emergency"); - if (queueManager.canAfford("emergency", cost)) - break; - } - } - let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; - if (autogarrison) - metadata.garrisonType = "protection"; - gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); - return true; -}; - -m.HQ.prototype.canBuild = function(gameState, structure) -{ - let type = gameState.applyCiv(structure); - if (this.buildManager.isUnbuildable(gameState, type)) - return false; - - if (gameState.isTemplateDisabled(type)) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); - return false; - } - - let template = gameState.getTemplate(type); - if (!template) - { - this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); - return false; - } - - if (!template.available(gameState)) - { - this.buildManager.setUnbuildable(gameState, type, 30, "tech"); - return false; - } - - if (!this.buildManager.hasBuilder(type)) - { - this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); - return false; - } - - if (this.numActiveBases() < 1) - { - // if no base, check that we can build outside our territory - let buildTerritories = template.buildTerritories(); - if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) - { - this.buildManager.setUnbuildable(gameState, type, 180, "room"); - return false; - } - } - - // build limits - let limits = gameState.getEntityLimits(); - let category = template.buildCategory(); - if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) - { - this.buildManager.setUnbuildable(gameState, type, 90, "limit"); - return false; - } - - return true; -}; - -m.HQ.prototype.updateTerritories = function(gameState) -{ - const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; - let alliedVictory = gameState.getAlliedVictory(); - let passabilityMap = gameState.getPassabilityMap(); - let width = this.territoryMap.width; - let cellSize = this.territoryMap.cellSize; - let insideSmall = Math.round(45 / cellSize); - let insideLarge = Math.round(80 / cellSize); // should be about the range of towers - let expansion = 0; - - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.borderMap.map[j] & m.outside_Mask) - continue; - if (this.borderMap.map[j] & m.fullFrontier_Mask) - this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier - - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - // If this tile was already accounted, remove it - if (this.basesMap.map[j] == 0) - continue; - let base = this.getBaseByID(this.basesMap.map[j]); - if (base) - { - let index = base.territoryIndices.indexOf(j); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); - } - else - API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); - this.basesMap.map[j] = 0; - } - else - { - // Update the frontier - let ix = j%width; - let iz = Math.floor(j/width); - let onFrontier = false; - for (let a of around) - { - let jx = ix + Math.round(insideSmall*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(insideSmall*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - { - this.borderMap.map[j] |= m.narrowFrontier_Mask; - break; - } - jx = ix + Math.round(insideLarge*a[0]); - if (jx < 0 || jx >= width) - continue; - jz = iz + Math.round(insideLarge*a[1]); - if (jz < 0 || jz >= width) - continue; - if (this.borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); - if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) - onFrontier = true; - } - if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask)) - this.borderMap.map[j] |= m.largeFrontier_Mask; - - // If this tile was not already accounted, add it. - if (this.basesMap.map[j] != 0) - continue; - let landPassable = false; - let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); - let access; - for (let k of ind) - { - if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - continue; - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - continue; - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - expansion++; - } - } - - if (!expansion) - return; - // We've increased our territory, so we may have some new room to build - this.buildManager.resetMissingRoom(gameState); - // And if sufficient expansion, check if building a new market would improve our present trade routes - let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; - if (expansion * cellArea > 960) - this.tradeManager.routeProspection = true; -}; - -/** Reassign territories when a base is going to be deleted */ -m.HQ.prototype.reassignTerritories = function(deletedBase) -{ - let cellSize = this.territoryMap.cellSize; - let width = this.territoryMap.width; - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != deletedBase.accessIndex) - continue; - let dist = API3.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; - } -}; - -/** - * returns the base corresponding to baseID - */ -m.HQ.prototype.getBaseByID = function(baseID) -{ - for (let base of this.baseManagers) - if (base.ID == baseID) - return base; - - return undefined; -}; - -/** - * returns the number of bases with a cc - * ActiveBases includes only those with a built cc - * PotentialBases includes also those with a cc in construction - */ -m.HQ.prototype.numActiveBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; -}; - -m.HQ.prototype.numPotentialBases = function() -{ - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -m.HQ.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (let base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } -}; - -m.HQ.prototype.resetBaseCache = function() -{ - this.turnCache.base = undefined; -}; - -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). - */ -m.HQ.prototype.assignGatherers = function() -{ - for (let base of this.baseManagers) - { - for (let worker of base.workers.values()) - { - if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE") - continue; - let orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); - } - } -}; - -m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) -{ - return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); -}; - -/** Check that the chosen position is not too near from an invading army */ -m.HQ.prototype.isNearInvadingArmy = function(pos) -{ - for (let army of this.defenseManager.armies) - if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) - return true; - return false; -}; - -m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) -{ - if (!this.turnCache.firingStructures) - this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); - for (let ent of this.turnCache.firingStructures.values()) - { - let range = radius + ent.attackRange("Ranged").max; - if (API3.SquareVectorDistance(ent.position(), pos) < range*range) - return true; - } - return false; -}; - -/** Compute the capture strength of all units attacking a capturable target */ -m.HQ.prototype.updateCaptureStrength = function(gameState) -{ - this.capturableTargets.clear(); - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.canCapture()) - continue; - let state = ent.unitAIState(); - if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") - continue; - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].target) - continue; - let targetId = orderData[0].target; - let target = gameState.getEntityById(targetId); - if (!target || !target.isCapturable() || !ent.canCapture(target)) - continue; - if (!this.capturableTargets.has(targetId)) - this.capturableTargets.set(targetId, { - "strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"), - "ents": new Set([ent.id()]) - }); - else - { - let capturableTarget = this.capturableTargets.get(target.id()); - capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); - capturableTarget.ents.add(ent.id()); - } - } - - for (let [targetId, capturableTarget] of this.capturableTargets) - { - let target = gameState.getEntityById(targetId); - let allowCapture; - for (let entId of capturableTarget.ents) - { - let ent = gameState.getEntityById(entId); - if (allowCapture === undefined) - allowCapture = m.allowCapture(gameState, ent, target); - let orderData = ent.unitAIOrderData(); - if (!orderData || !orderData.length || !orderData[0].attackType) - continue; - if ((orderData[0].attackType == "Capture") !== allowCapture) - ent.attack(targetId, allowCapture); - } - } - - this.capturableTargetsTime = gameState.ai.elapsedTime; -}; - -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** add a gatherer to the turn cache for this supply. */ -m.HQ.prototype.AddTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) - ++this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = 1; - } -}; - -/** remove a gatherer to the turn cache for this supply. */ -m.HQ.prototype.RemoveTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - --this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = -1; - } -}; - -m.HQ.prototype.GetTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - return this.turnCache.resourceGatherer[supplyID]; - - return 0; -}; - -/** The next two are to register that we assigned a gatherer to a resource this turn. */ -m.HQ.prototype.AddTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - ++this.turnCache["resourceGatherer-" + resource]; - else - this.turnCache["resourceGatherer-" + resource] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -m.HQ.prototype.GetTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - return this.turnCache["resourceGatherer-" + resource]; - - return 0; -}; - -/** - * flag a resource as exhausted - */ -m.HQ.prototype.isResourceExhausted = function(resource) -{ - if (this.turnCache["exhausted-" + resource] == undefined) - this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); - - return this.turnCache["exhausted-" + resource]; -}; - -/** - * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) - */ -m.HQ.prototype.isDefendable = function(ent) -{ - if (!this.turnCache.numAround) - this.turnCache.numAround = {}; - if (this.turnCache.numAround[ent.id()] === undefined) - this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); - return +this.turnCache.numAround[ent.id()] > 8; -}; - -/** - * Get the number of population already accounted for - */ -m.HQ.prototype.getAccountedPopulation = function(gameState) -{ - if (this.turnCache.accountedPopulation == undefined) - { - let pop = gameState.getPopulation(); - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.unitTemplate) - continue; - let 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 - */ -m.HQ.prototype.getAccountedWorkers = function(gameState) -{ - if (this.turnCache.accountedWorkers == undefined) - { - let workers = gameState.getOwnEntitiesByRole("worker", true).length; - for (let ent of gameState.getOwnTrainingFacilities().values()) - { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") - continue; - workers += item.count; - } - } - this.turnCache.accountedWorkers = workers; - } - return this.turnCache.accountedWorkers; -}; - -/** - * Some functions are run every turn - * Others once in a while - */ -m.HQ.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Headquarters update"); - this.turnCache = {}; - this.territoryMap = m.createTerritoryMap(gameState); - this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities(); - // TODO find a better way to update - if (this.currentPhase != gameState.currentPhase()) - { - if (this.Config.debug > 0) - API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + - " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + - " phasing " + this.phasing); - this.currentPhase = gameState.currentPhase(); - - // In principle, this.phasing should be already reset to 0 when starting the research - // but this does not work in case of an autoResearch tech - if (this.phasing) - this.phasing = 0; - } - -/* if (this.Config.debug > 1) - { - gameState.getOwnUnits().forEach (function (ent) { - if (!ent.position()) - return; - m.dumpEntity(ent); - }); - } */ - - this.checkEvents(gameState, events); - this.navalManager.checkEvents(gameState, queues, events); - - if (this.phasing) - this.checkPhaseRequirements(gameState, queues); - else - this.researchManager.checkPhase(gameState, queues); - - if (this.numActiveBases() > 0) - { - if (gameState.ai.playedTurn % 4 == 0) - this.trainMoreWorkers(gameState, queues); - - if (gameState.ai.playedTurn % 4 == 1) - this.buildMoreHouses(gameState, queues); - - if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) - this.buildFarmstead(gameState, queues); - - if (this.needCorral && gameState.ai.playedTurn % 4 == 3) - this.manageCorral(gameState, queues); - - if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) - this.researchManager.update(gameState, queues); - } - - if (this.numPotentialBases() < 1 || - this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) - this.checkBaseExpansion(gameState, queues); - - if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) - { - if (!this.canBarter) - this.buildMarket(gameState, queues); - - if (!this.saveResources) - { - this.buildBlacksmith(gameState, queues); - this.buildTemple(gameState, queues); - } - - if (gameState.ai.playedTurn % 30 == 0 && - gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) - this.buildWonder(gameState, queues, false); - } - - this.tradeManager.update(gameState, events, queues); - - this.garrisonManager.update(gameState, events); - this.defenseManager.update(gameState, events); - - if (gameState.ai.playedTurn % 3 == 0) - { - this.constructTrainingBuildings(gameState, queues); - if (this.Config.difficulty > 0) - this.buildDefenses(gameState, queues); - } - - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase; // We will loop only on 1 active base per turn - do - { - 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]); - } - while (!activeBase && nbBases != 0); - - this.navalManager.update(gameState, queues, events); - - if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) - this.attackManager.update(gameState, queues, events); - - this.diplomacyManager.update(gameState, events); - - this.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(); -}; - -m.HQ.prototype.Serialize = function() -{ - let properties = { - "phasing": this.phasing, - "currentBase": this.currentBase, - "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 - }; - - let baseManagers = []; - for (let base of this.baseManagers) - baseManagers.push(base.Serialize()); - - if (this.Config.debug == -100) - { - API3.warn(" HQ serialization ---------------------"); - API3.warn(" properties " + uneval(properties)); - API3.warn(" baseManagers " + uneval(baseManagers)); - API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); - API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); - API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); - API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); - API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); - API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); - API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); - API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); - API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); - } - - return { - "properties": properties, - - "baseManagers": baseManagers, - "attackManager": this.attackManager.Serialize(), - "buildManager": this.buildManager.Serialize(), - "defenseManager": this.defenseManager.Serialize(), - "tradeManager": this.tradeManager.Serialize(), - "navalManager": this.navalManager.Serialize(), - "researchManager": this.researchManager.Serialize(), - "diplomacyManager": this.diplomacyManager.Serialize(), - "garrisonManager": this.garrisonManager.Serialize(), - "victoryManager": this.victoryManager.Serialize(), - }; -}; - -m.HQ.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.baseManagers = []; - for (let base of data.baseManagers) - { - // the first call to deserialize set the ID base needed by entitycollections - let newbase = new m.BaseManager(gameState, this.Config); - newbase.Deserialize(gameState, base); - newbase.init(gameState); - newbase.Deserialize(gameState, base); - this.baseManagers.push(newbase); - } - - this.navalManager = new m.NavalManager(this.Config); - this.navalManager.init(gameState, true); - this.navalManager.Deserialize(gameState, data.navalManager); - - this.attackManager = new m.AttackManager(this.Config); - this.attackManager.Deserialize(gameState, data.attackManager); - this.attackManager.init(gameState); - this.attackManager.Deserialize(gameState, data.attackManager); - - this.buildManager = new m.BuildManager(); - this.buildManager.Deserialize(data.buildManager); - - this.defenseManager = new m.DefenseManager(this.Config); - this.defenseManager.Deserialize(gameState, data.defenseManager); - - this.tradeManager = new m.TradeManager(this.Config); - this.tradeManager.init(gameState); - this.tradeManager.Deserialize(gameState, data.tradeManager); - - this.researchManager = new m.ResearchManager(this.Config); - this.researchManager.Deserialize(data.researchManager); - - this.diplomacyManager = new m.DiplomacyManager(this.Config); - this.diplomacyManager.Deserialize(data.diplomacyManager); - - this.garrisonManager = new m.GarrisonManager(this.Config); - this.garrisonManager.Deserialize(data.garrisonManager); - - this.victoryManager = new m.VictoryManager(this.Config); - this.victoryManager.Deserialize(data.victoryManager); -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-unitary/mapModule.js b/install/petraBased/petra-unitary/mapModule.js deleted file mode 100644 index 225767a..0000000 --- a/install/petraBased/petra-unitary/mapModule.js +++ /dev/null @@ -1,221 +0,0 @@ -var PETRA = function(m) -{ - -/** map functions */ - -m.TERRITORY_PLAYER_MASK = 0x1F; -m.TERRITORY_BLINKING_MASK = 0x40; - -m.createObstructionMap = function(gameState, accessIndex, template) -{ - let passabilityMap = gameState.getPassabilityMap(); - let territoryMap = gameState.ai.territoryMap; - let ratio = territoryMap.cellSize / passabilityMap.cellSize; - - // default values - let placementType = "land"; - let buildOwn = true; - let buildAlly = true; - let buildNeutral = true; - let buildEnemy = false; - // If there is a template then replace the defaults - if (template) - { - placementType = template.buildPlacementType(); - buildOwn = template.hasBuildTerritory("own"); - buildAlly = template.hasBuildTerritory("ally"); - buildNeutral = template.hasBuildTerritory("neutral"); - buildEnemy = template.hasBuildTerritory("enemy"); - } - let obstructionTiles = new Uint8Array(passabilityMap.data.length); - - let passMap; - let obstructionMask; - if (placementType == "shore") - { - passMap = gameState.ai.accessibility.navalPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-shore"); - } - else - { - passMap = gameState.ai.accessibility.landPassMap; - obstructionMask = gameState.getPassabilityClassMask("building-land"); - } - - for (let k = 0; k < territoryMap.data.length; ++k) - { - let tilePlayer = territoryMap.data[k] & m.TERRITORY_PLAYER_MASK; - let isConnected = (territoryMap.data[k] & m.TERRITORY_BLINKING_MASK) == 0; - if (tilePlayer === PlayerID) - { - if (!buildOwn || !buildNeutral && !isConnected) - continue; - } - else if (gameState.isPlayerMutualAlly(tilePlayer)) - { - if (!buildAlly || !buildNeutral && !isConnected) - continue; - } - else if (tilePlayer === 0) - { - if (!buildNeutral) - continue; - } - else - { - if (!buildEnemy) - continue; - } - - let x = ratio * (k % territoryMap.width); - let y = ratio * Math.floor(k / territoryMap.width); - for (let ix = 0; ix < ratio; ++ix) - { - for (let iy = 0; iy < ratio; ++iy) - { - let i = x + ix + (y + iy)*passabilityMap.width; - if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) - continue; - if (!(passabilityMap.data[i] & obstructionMask)) - obstructionTiles[i] = 255; - } - } - } - - let map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); - map.setMaxVal(255); - - if (template && template.buildDistance()) - { - let distance = template.buildDistance(); - let minDist = distance.MinDistance ? +distance.MinDistance : 0; - if (minDist) - { - let obstructionRadius = template.obstructionRadius(); - if (obstructionRadius) - minDist -= obstructionRadius.min; - let fromClass = distance.FromClass; - let cellSize = passabilityMap.cellSize; - let cellDist = 1 + minDist / cellSize; - let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); - for (let ent of structures.values()) - { - if (!ent.position()) - continue; - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - map.addInfluence(x, z, cellDist, -255, "constant"); - } - } - } - - return map; -}; - - -m.createTerritoryMap = function(gameState) -{ - let map = gameState.ai.territoryMap; - - let ret = new API3.Map(gameState.sharedScript, "territory", map.data); - ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; - ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; - ret.isBlinking = function(p) { return (this.point(p) & m.TERRITORY_BLINKING_MASK) != 0; }; - return ret; -}; - -/** - * The borderMap contains some border and frontier information: - * - border of the map filled once: - * - all mini-cells (1x1) from the big cell (8x8) inaccessibles => bit 0 - * - inside a given distance to the border => bit 1 - * - frontier of our territory (updated regularly in updateFrontierMap) - * - narrow border (inside our territory) => bit 2 - * - large border (inside our territory, exclusive of narrow) => bit 3 - */ - -m.outside_Mask = 1; -m.border_Mask = 2; -m.fullBorder_Mask = m.outside_Mask | m.border_Mask; -m.narrowFrontier_Mask = 4; -m.largeFrontier_Mask = 8; -m.fullFrontier_Mask = m.narrowFrontier_Mask | m.largeFrontier_Mask; - -m.createBorderMap = function(gameState) -{ - let map = new API3.Map(gameState.sharedScript, "territory"); - let width = map.width; - let border = Math.round(80 / map.cellSize); - let passabilityMap = gameState.getPassabilityMap(); - let obstructionMask = gameState.getPassabilityClassMask("unrestricted"); - if (gameState.circularMap) - { - let ic = (width - 1) / 2; - let radcut = (ic - border) * (ic - border); - for (let j = 0; j < map.length; ++j) - { - let dx = j%width - ic; - let dy = Math.floor(j/width) - ic; - let radius = dx*dx + dy*dy; - if (radius < radcut) - continue; - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - else - { - let borderCut = width - border; - for (let j = 0; j < map.length; ++j) - { - let ix = j%width; - let iy = Math.floor(j/width); - if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) - { - map.map[j] = m.outside_Mask; - let ind = API3.getMapIndices(j, map, passabilityMap); - for (let k of ind) - { - if (passabilityMap.data[k] & obstructionMask) - continue; - map.map[j] = m.border_Mask; - break; - } - } - } - } - -// map.dumpIm("border.png", 5); - return map; -}; - -m.debugMap = function(gameState, map) -{ - let width = map.width; - let cell = map.cellSize; - gameState.getEntities().forEach(ent => { - let pos = ent.position(); - if (!pos) - return; - let x = Math.round(pos[0] / cell); - let z = Math.round(pos[1] / cell); - let id = x + width*z; - if (map.map[id] == 1) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else if (map.map[id] == 2) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - else if (map.map[id] == 3) - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 0, 2] }); - }); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/navalManager.js b/install/petraBased/petra-unitary/navalManager.js deleted file mode 100644 index 5a16d0f..0000000 --- a/install/petraBased/petra-unitary/navalManager.js +++ /dev/null @@ -1,896 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Naval Manager - * Will deal with anything ships. - * -Basically trade over water (with fleets and goals commissioned by the economy manager) - * -Defense over water (commissioned by the defense manager) - * -Transport of units over water (a few units). - * -Scouting, ultimately. - * Also deals with handling docks, making sure we have access and stuffs like that. - */ - -m.NavalManager = function(Config) -{ - this.Config = Config; - - // ship subCollections. Also exist for land zones, idem, not caring. - this.seaShips = []; - this.seaTransportShips = []; - this.seaWarShips = []; - this.seaFishShips = []; - - // wanted NB per zone. - this.wantedTransportShips = []; - this.wantedWarShips = []; - this.wantedFishShips = []; - // needed NB per zone. - this.neededTransportShips = []; - this.neededWarShips = []; - - this.transportPlans = []; - - // shore-line regions where we can load and unload units - this.landingZones = {}; -}; - -/** More initialisation for stuff that needs the gameState */ -m.NavalManager.prototype.init = function(gameState, deserializing) -{ - // docks - this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"])); - this.docks.registerUpdates(); - - this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); - // note: those two can overlap (some transport ships are warships too and vice-versa). - this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); - this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); - this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); - - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - this.warShips.registerUpdates(); - this.fishShips.registerUpdates(); - - let availableFishes = {}; - for (let fish of gameState.getFishableSupplies().values()) - { - let sea = this.getFishSea(gameState, fish); - if (sea && availableFishes[sea]) - availableFishes[sea] += fish.resourceSupplyAmount(); - else if (sea) - availableFishes[sea] = fish.resourceSupplyAmount(); - } - - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (!gameState.ai.HQ.navalRegions[i]) - { - // push dummies - this.seaShips.push(undefined); - this.seaTransportShips.push(undefined); - this.seaWarShips.push(undefined); - this.seaFishShips.push(undefined); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - else - { - let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaShips.push(collec); - collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaTransportShips.push(collec); - collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaWarShips.push(collec); - collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); - collec.registerUpdates(); - this.seaFishShips.push(collec); - this.wantedTransportShips.push(0); - this.wantedWarShips.push(0); - if (availableFishes[i] && availableFishes[i] > 1000) - this.wantedFishShips.push(this.Config.Economy.targetNumFishers); - else - this.wantedFishShips.push(0); - this.neededTransportShips.push(0); - this.neededWarShips.push(0); - } - } - - if (deserializing) - return; - - // determination of the possible landing zones - let width = gameState.getPassabilityMap().width; - let length = width * gameState.getPassabilityMap().height; - for (let i = 0; i < length; ++i) - { - let land = gameState.ai.accessibility.landPassMap[i]; - if (land < 2) - continue; - let naval = gameState.ai.accessibility.navalPassMap[i]; - if (naval < 2) - continue; - if (!this.landingZones[land]) - this.landingZones[land] = {}; - if (!this.landingZones[land][naval]) - this.landingZones[land][naval] = new Set(); - this.landingZones[land][naval].add(i); - } - // and keep only thoses with enough room around when possible - for (let land in this.landingZones) - { - for (let sea in this.landingZones[land]) - { - let landing = this.landingZones[land][sea]; - let nbaround = {}; - let nbcut = 0; - for (let i of landing) - { - let nb = 0; - if (landing.has(i-1)) - nb++; - if (landing.has(i+1)) - nb++; - if (landing.has(i+width)) - nb++; - if (landing.has(i-width)) - nb++; - nbaround[i] = nb; - nbcut = Math.max(nb, nbcut); - } - nbcut = Math.min(2, nbcut); - for (let i of landing) - { - if (nbaround[i] < nbcut) - landing.delete(i); - } - } - } - - // Assign our initial docks and ships - for (let ship of this.ships.values()) - m.setSeaAccess(gameState, ship); - for (let dock of this.docks.values()) - m.setSeaAccess(gameState, dock); -}; - -m.NavalManager.prototype.updateFishingBoats = function(sea, num) -{ - if (this.wantedFishShips[sea]) - this.wantedFishShips[sea] = num; -}; - -m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) -{ - if (sea !== undefined) - this.wantedFishShips[sea] = 0; - else - this.wantedFishShips.fill(0); -}; - -/** Get the sea, cache it if not yet done and check if in opensea */ -m.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]]; - let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); - let width = gameState.ai.accessibility.width; - let k = pos[0] + pos[1]*width; - sea = gameState.ai.accessibility.navalPassMap[k]; - fish.setMetadata(PlayerID, "sea", sea); - let radius = 120 / gameState.ai.accessibility.cellSize / ntry; - if (around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let 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) - { - let 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 */ -m.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]]; - let territoryMap = gameState.ai.HQ.territoryMap; - let width = territoryMap.width; - let radius = 120 / territoryMap.cellSize / ntry; - let pos = territoryMap.gamePosToMapPos(fish.position()); - return around.every(a => { - for (let t = 0; t < ntry; ++t) - { - let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); - let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); - if (i < 0 || i >= width || j < 0 || j >= width) - continue; - let 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 */ -m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) -{ - let seas = gameState.ai.accessibility.regionLinks[region].slice(); - this.docks.forEach(dock => { - if (!dock.hasClass("Dock") || m.getLandAccess(gameState, dock) != region) - return; - let i = seas.indexOf(m.getSeaAccess(gameState, dock)); - if (i != -1) - seas.splice(i--, 1); - }); - return seas; -}; - -m.NavalManager.prototype.checkEvents = function(gameState, queues, events) -{ - for (let evt of events.Create) - { - if (!evt.entity) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } - - for (let evt of events.TrainingFinished) - { - if (!evt.entities) - continue; - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) - continue; - m.setSeaAccess(gameState, ent); - } - } - - for (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) - continue; - if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) - continue; - let plan = this.getPlan(evt.metadata[PlayerID].transporter); - if (!plan) - continue; - - let shipId = evt.entityObj.id(); - if (this.Config.debug > 1) - API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); - if (plan.state == "boarding") - { - // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan - plan.units.forEach(ent => { - if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() || - ent.getMetadata(PlayerID, "onBoard") == shipId) - ent.setMetadata(PlayerID, "onBoard", undefined); - }); - plan.needTransportShips = !plan.transportShips.hasEntities(); - } - else if (plan.state == "sailing") - { - let endIndex = plan.endIndex; - for (let ent of plan.units.values()) - { - if (!ent.position()) // unit from another ship of this plan ... do nothing - continue; - let access = m.getLandAccess(gameState, ent); - let endPos = ent.getMetadata(PlayerID, "endPos"); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // nothing else to do if access = endIndex as already at destination - // otherwise, we should require another transport - // TODO if attacking and no more ships available, remove the units from the attack - // to avoid delaying it too much - if (access != endIndex) - this.requireTransport(gameState, ent, access, endIndex, endPos); - } - } - } - - for (let evt of events.OwnershipChanged) // capture events - { - if (evt.to !== PlayerID) - continue; - let ent = gameState.getEntityById(evt.entity); - if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - m.setSeaAccess(gameState, ent); - } -}; - - -m.NavalManager.prototype.getPlan = function(ID) -{ - for (let plan of this.transportPlans) - if (plan.ID === ID) - return plan; - return undefined; -}; - -m.NavalManager.prototype.addPlan = function(plan) -{ - this.transportPlans.push(plan); -}; - -/** - * complete already existing plan or create a new one for this requirement - * (many units can then call this separately and end up in the same plan) - * TODO check garrison classes - */ -m.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) -{ - if (!ent.canGarrison()) - return false; - - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - if (this.Config.debug > 0) - API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport"); - return false; - } - - let plans = []; - for (let plan of this.transportPlans) - { - if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding") - continue; - // Limit the number of siege units per transport to avoid problems when ungarrisoning - if (m.isSiegeUnit(ent) && plan.units.filter(unit => m.isSiegeUnit(unit)).length > 3) - continue; - plans.push(plan); - } - - if (plans.length) - { - plans.sort(plan => plan.units.length); - plans[0].addUnit(ent, endPos); - return true; - } - - let plan = new m.TransportPlan(gameState, [ent], startIndex, endIndex, endPos); - if (plan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> transport plan aborted <<<<"); - return false; - } - plan.init(gameState); - this.transportPlans.push(plan); - return true; -}; - -/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ -m.NavalManager.prototype.splitTransport = function(gameState, plan) -{ - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan started <<<<"); - let newplan = new m.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); - if (newplan.failed) - { - if (this.Config.debug > 1) - API3.warn(">>>> split of transport plan aborted <<<<"); - return false; - } - newplan.init(gameState); - - for (let 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 - */ -m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) -{ - let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); - if (fromAccess !== 1) - return; - if (toAccess < 2) - return; - - for (let ship of this.ships.values()) - { - if (!ship.isGarrisonHolder() || !ship.garrisoned().length) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined) - continue; - let units = []; - for (let entId of ship.garrisoned()) - units.push(gameState.getEntityById(entId)); - // TODO check that the garrisoned units have not another purpose - let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); - if (plan.failed) - continue; - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - -// set minimal number of needed ships when a new event (new base or new attack plan) -m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) -{ - if (!sea) - return; - if (this.wantedTransportShips[sea] < number) - this.wantedTransportShips[sea] = number; -}; - -// bumps up the number of ships we want if we need more. -m.NavalManager.prototype.checkLevels = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - this.neededTransportShips[sea] = 0; - - for (let plan of this.transportPlans) - { - if (!plan.needTransportShips || plan.units.length < 2) - continue; - let sea = plan.sea; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || - this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - continue; - ++this.neededTransportShips[sea]; - if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) - { - ++this.wantedTransportShips[sea]; - return; - } - } - - for (let sea = 0; sea < this.neededTransportShips.length; sea++) - if (this.neededTransportShips[sea] > 2) - ++this.wantedTransportShips[sea]; -}; - -m.NavalManager.prototype.maintainFleet = function(gameState, queues) -{ - if (queues.ships.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) - return; - // check if we have enough transport ships per region. - for (let sea = 0; sea < this.seaShips.length; ++sea) - { - if (this.seaShips[sea] === undefined) - continue; - if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) - continue; - - if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) - { - let template = this.getBestShip(gameState, sea, "transport"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); - continue; - } - } - - - if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) - { - let template = this.getBestShip(gameState, sea, "fishing"); - if (template) - { - queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); - continue; - } - } - } -}; - -/** assigns free ships to plans that need some */ -m.NavalManager.prototype.assignShipsToPlans = function(gameState) -{ - for (let plan of this.transportPlans) - if (plan.needTransportShips) - plan.assignShip(gameState); -}; - -/** Return true if this ship is likeky (un)garrisoning units */ -m.NavalManager.prototype.isShipBoarding = function(ship) -{ - if (!ship.position()) - return false; - let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); - if (!plan || !plan.boardingPos[ship.id()]) - return false; - return API3.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. - */ -m.NavalManager.prototype.moveApart = function(gameState) -{ - let blockedShips = []; - let blockedIds = []; - - for (let ship of this.ships.values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) - continue; - - let unitAIState = ship.unitAIState(); - if (ship.getMetadata(PlayerID, "transporter") !== undefined || - unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - - } - } - - for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - let shipPosition = ship.position(); - if (!shipPosition) - continue; - let role = ship.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - let 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, 30); - blockedShips.push(ship); - blockedIds.push(ship.id()); - } - else if (ship.isIdle()) - { - let 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 (m.gatherTreasure(gameState, ship, true)) - continue; - // Do not stay idle near a dock to not disturb other ships - let sea = ship.getMetadata(PlayerID, "sea"); - for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) - { - if (m.getSeaAccess(gameState, dock) != sea) - continue; - if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) - continue; - ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70); - } - } - } - - for (let ship of blockedShips) - { - let shipPosition = ship.position(); - let sea = ship.getMetadata(PlayerID, "sea"); - for (let blockingShip of this.seaShips[sea].values()) - { - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && - unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && - unitAIState != "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - - for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) - { - if (blockingShip.getMetadata(PlayerID, "sea") != sea) - continue; - if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) - continue; - let role = blockingShip.getMetadata(PlayerID, "role"); - if (!role || role != "trader") // already accounted before - continue; - let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); - let unitAIState = blockingShip.unitAIState(); - if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") - { - if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); - } - else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); - } - } -}; - -m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) -{ - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) - return; - - if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) - { - if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && - !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() && - gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock")) - { - let dockStarted = false; - for (let base of gameState.ai.HQ.baseManagers) - { - if (dockStarted) - break; - if (!base.anchor || base.constructing) - continue; - let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); - for (let sea of remaining) - { - if (!gameState.ai.HQ.navalRegions[sea]) - continue; - let wantedLand = {}; - wantedLand[base.accessIndex] = true; - queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); - dockStarted = true; - break; - } - } - } - } - - if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || - queues.militaryBuilding.hasQueuedUnits()) - return; - if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || - this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) - return; - // Use in priority resources to build a market - if (!gameState.getOwnEntitiesByClass("BarterMarket", 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; - let wantedLand = {}; - for (let base of gameState.ai.HQ.baseManagers) - if (base.anchor) - wantedLand[base.accessIndex] = true; - let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); -}; - -/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ -m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) -{ - let civ = gameState.getPlayerCiv(); - let trainableShips = []; - gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { - let trainables = ent.trainableEntities(civ); - for (let trainable of trainables) - { - if (gameState.isTemplateDisabled(trainable)) - continue; - let template = gameState.getTemplate(trainable); - if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) - trainableShips.push(trainable); - } - }); - - let best = 0; - let bestShip; - let limits = gameState.getEntityLimits(); - let current = gameState.getEntityCounts(); - for (let trainable of trainableShips) - { - let template = gameState.getTemplate(trainable); - if (!template.available(gameState)) - continue; - - let category = template.trainingCategory(); - if (category && limits[category] && current[category] >= limits[category]) - continue; - - let arrows = +(template.getDefaultArrow() || 0); - if (goal === "attack") // choose the maximum default arrows - { - if (best > arrows) - continue; - best = arrows; - } - else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport - { - let capacity = +(template.garrisonMax() || 0); - if (capacity < 2) - continue; - capacity += 10*arrows; - if (MatchesClassList(template.garrisonableClasses(), "Siege")) - capacity += 50; - if (best > capacity) - continue; - best = capacity; - } - else if (goal === "fishing") - if (!template.hasClass("FishingBoat")) - continue; - bestShip = trainable; - } - return bestShip; -}; - -m.NavalManager.prototype.update = function(gameState, queues, events) -{ - Engine.ProfileStart("Naval Manager update"); - - // close previous transport plans if finished - for (let i = 0; i < this.transportPlans.length; ++i) - { - let remaining = this.transportPlans[i].update(gameState); - if (remaining) - continue; - if (this.Config.debug > 1) - API3.warn("no more units on transport plan " + this.transportPlans[i].ID); - this.transportPlans[i].releaseAll(); - this.transportPlans.splice(i--, 1); - } - // assign free ships to plans which need them - this.assignShipsToPlans(gameState); - - // and require for more ships/structures if needed - if (gameState.ai.playedTurn % 3 === 0) - { - this.checkLevels(gameState, queues); - this.maintainFleet(gameState, queues); - this.buildNavalStructures(gameState, queues); - } - // let inactive ships move apart from active ones (waiting for a better pathfinder) - this.moveApart(gameState); - - Engine.ProfileStop(); -}; - -m.NavalManager.prototype.Serialize = function() -{ - let properties = { - "wantedTransportShips": this.wantedTransportShips, - "wantedWarShips": this.wantedWarShips, - "wantedFishShips": this.wantedFishShips, - "neededTransportShips": this.neededTransportShips, - "neededWarShips": this.neededWarShips, - "landingZones": this.landingZones - }; - - let transports = {}; - for (let plan in this.transportPlans) - transports[plan] = this.transportPlans[plan].Serialize(); - - return { "properties": properties, "transports": transports }; -}; - -m.NavalManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data.properties) - this[key] = data.properties[key]; - - this.transportPlans = []; - for (let i in data.transports) - { - let dataPlan = data.transports[i]; - let plan = new m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); - plan.Deserialize(dataPlan); - plan.init(gameState); - this.transportPlans.push(plan); - } -}; - - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/queue.js b/install/petraBased/petra-unitary/queue.js deleted file mode 100644 index b11b2e1..0000000 --- a/install/petraBased/petra-unitary/queue.js +++ /dev/null @@ -1,167 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Holds a list of wanted plans to train or construct - */ - -m.Queue = function() -{ - this.plans = []; - this.paused = false; - this.switched = 0; -}; - -m.Queue.prototype.empty = function() -{ - this.plans = []; -}; - -m.Queue.prototype.addPlan = function(newPlan) -{ - if (!newPlan) - return; - for (let 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); -}; - -m.Queue.prototype.check= function(gameState) -{ - while (this.plans.length > 0) - { - if (!this.plans[0].isInvalid(gameState)) - return; - let plan = this.plans.shift(); - if (plan.queueToReset) - gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]); - } -}; - -m.Queue.prototype.getNext = function() -{ - if (this.plans.length > 0) - return this.plans[0]; - return null; -}; - -m.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 - */ -m.Queue.prototype.maxAccountWanted = function(gameState, fraction) -{ - let cost = new API3.Resources(); - 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) - { - let costs = this.plans[1].getCost(); - costs.multiply(fraction); - cost.add(costs); - } - return cost; -}; - -m.Queue.prototype.queueCost = function() -{ - let cost = new API3.Resources(); - for (let plan of this.plans) - cost.add(plan.getCost()); - return cost; -}; - -m.Queue.prototype.length = function() -{ - return this.plans.length; -}; - -m.Queue.prototype.hasQueuedUnits = function() -{ - return this.plans.length > 0; -}; - -m.Queue.prototype.countQueuedUnits = function() -{ - let count = 0; - for (let plan of this.plans) - count += plan.number; - return count; -}; - -m.Queue.prototype.hasQueuedUnitsWithClass = function(classe) -{ - return this.plans.some(plan => plan.template && plan.template.hasClass(classe)); -}; - -m.Queue.prototype.countQueuedUnitsWithClass = function(classe) -{ - let count = 0; - for (let plan of this.plans) - if (plan.template && plan.template.hasClass(classe)) - count += plan.number; - return count; -}; - -m.Queue.prototype.countQueuedUnitsWithMetadata = function(data, value) -{ - let count = 0; - for (let plan of this.plans) - if (plan.metadata[data] && plan.metadata[data] == value) - count += plan.number; - return count; -}; - -m.Queue.prototype.Serialize = function() -{ - let plans = []; - for (let plan of this.plans) - plans.push(plan.Serialize()); - - return { "plans": plans, "paused": this.paused, "switched": this.switched }; -}; - -m.Queue.prototype.Deserialize = function(gameState, data) -{ - this.paused = data.paused; - this.switched = data.switched; - this.plans = []; - for (let dataPlan of data.plans) - { - let plan; - if (dataPlan.category == "unit") - plan = new m.TrainingPlan(gameState, dataPlan.type); - else if (dataPlan.category == "building") - plan = new m.ConstructionPlan(gameState, dataPlan.type); - else if (dataPlan.category == "technology") - plan = new m.ResearchPlan(gameState, dataPlan.type); - else - { - API3.warn("Petra deserialization error: plan unknown " + uneval(dataPlan)); - continue; - } - plan.Deserialize(gameState, dataPlan); - this.plans.push(plan); - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/queueplan.js b/install/petraBased/petra-unitary/queueplan.js deleted file mode 100644 index ba4ebf5..0000000 --- a/install/petraBased/petra-unitary/queueplan.js +++ /dev/null @@ -1,70 +0,0 @@ -var PETRA = function(m) -{ -/** - * Common functions and variables to all queue plans. - */ - -m.QueuePlan = function(gameState, type, metadata) -{ - this.type = gameState.applyCiv(type); - this.metadata = metadata; - - this.template = gameState.getTemplate(this.type); - if (!this.template) - { - API3.warn("Tried to add the inexisting template " + this.type + " to Petra."); - return false; - } - this.ID = gameState.ai.uniqueIDs.plans++; - this.cost = new API3.Resources(this.template.cost()); - this.number = 1; - this.category = ""; - - return true; -}; - -/** Check the content of this queue */ -m.QueuePlan.prototype.isInvalid = function(gameState) -{ - return false; -}; - -/** if true, the queue manager will begin increasing this plan's account. */ -m.QueuePlan.prototype.isGo = function(gameState) -{ - return true; -}; - -/** can we start this plan immediately? */ -m.QueuePlan.prototype.canStart = function(gameState) -{ - return false; -}; - -/** process the plan. */ -m.QueuePlan.prototype.start = function(gameState) -{ - // should call onStart. -}; - -m.QueuePlan.prototype.getCost = function() -{ - let costs = new API3.Resources(); - costs.add(this.cost); - if (this.number !== 1) - costs.multiply(this.number); - return costs; -}; - -/** - * On Event functions. - * Can be used to do some specific stuffs - * Need to be updated to actually do something if you want them to. - * this is called by "Start" if it succeeds. - */ -m.QueuePlan.prototype.onStart = function(gameState) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/queueplanBuilding.js b/install/petraBased/petra-unitary/queueplanBuilding.js deleted file mode 100644 index 841d5d2..0000000 --- a/install/petraBased/petra-unitary/queueplanBuilding.js +++ /dev/null @@ -1,951 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Defines a construction plan, ie a building. - * We'll try to fing a good position if non has been provided - */ - -m.ConstructionPlan = function(gameState, type, metadata, position) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - return false; - - this.position = position ? position : 0; - - this.category = "building"; - - return true; -}; - -m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ConstructionPlan.prototype.canStart = function(gameState) -{ - if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn - return false; - - if (!this.isGo(gameState)) - return false; - - if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) - return false; - - return gameState.ai.HQ.buildManager.hasBuilder(this.type); -}; - -m.ConstructionPlan.prototype.start = function(gameState) -{ - Engine.ProfileStart("Building construction start"); - - // We don't care which builder we assign, since they won't actually do - // the building themselves - all we care about is that there is at least - // one unit that can start the foundation (should always be the case here). - let builder = gameState.findBuilder(this.type); - if (!builder) - { - API3.warn("petra error: builder not found when starting construction."); - Engine.ProfileStop(); - return; - } - - let pos = this.findGoodPosition(gameState); - if (!pos) - { - gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); - Engine.ProfileStop(); - return; - } - - if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("BarterMarket") || - gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())) - { - // Check if this market is still worth building (others may have been built making it useless) - let tradeManager = gameState.ai.HQ.tradeManager; - tradeManager.checkRoutes(gameState); - if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) - { - Engine.ProfileStop(); - return; - } - } - gameState.ai.HQ.turnCache.buildingBuilt = true; - - if (this.metadata === undefined) - this.metadata = { "base": pos.base }; - else if (this.metadata.base === undefined) - this.metadata.base = pos.base; - - if (pos.access) - this.metadata.access = pos.access; // needed for Docks whose position is on water - else - this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); - - if (this.template.buildPlacementType() == "shore") - { - // adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) - builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); - else // try with the lowest, move towards us unless we're same - { - for (let step = 0; step <= 1; step += 0.2) - builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, - pos.angle, this.metadata); - } - this.onStart(gameState); - Engine.ProfileStop(); - - if (this.metadata && this.metadata.proximity) - gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); -}; - -m.ConstructionPlan.prototype.findGoodPosition = function(gameState) -{ - let template = this.template; - - if (template.buildPlacementType() == "shore") - return this.findDockPosition(gameState); - - let HQ = gameState.ai.HQ; - if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) - { - // recompute the best dropsite location in case some conditions have changed - let base = HQ.getBaseByID(this.metadata.base); - let type = this.metadata.type ? this.metadata.type : "wood"; - let newpos = base.findBestDropsiteLocation(gameState, type); - if (newpos && newpos.quality > 0) - { - let pos = newpos.pos; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; - } - } - - if (!this.position) - { - if (template.hasClass("CivCentre")) - { - let pos; - if (this.metadata && this.metadata.resource) - { - let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; - pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); - } - else - pos = HQ.findStrategicCCLocation(gameState, template); - - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; - // No possible location, try to build instead a dock in a not-enemy island - let templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) - { - template = gameState.getTemplate(templateName); - if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) - this.buildOverseaDock(gameState, template); - } - return false; - } - else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) - { - let pos = HQ.findDefensiveLocation(gameState, template); - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - // if this fortress is our first one, just try the standard placement - if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) - return false; - } - else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before - { - let pos = HQ.findMarketLocation(gameState, template); - if (pos && pos[2] > 0) - { - if (!this.metadata) - this.metadata = {}; - this.metadata.expectedGain = pos[3]; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - } - else if (!pos) - return false; - } - } - - // Compute each tile's closeness to friendly structures: - - let placement = new API3.Map(gameState.sharedScript, "territory"); - let cellSize = placement.cellSize; // size of each tile - - let alreadyHasHouses = false; - - if (this.position) // If a position was specified then place the building as close to it as possible - { - let x = Math.floor(this.position[0] / cellSize); - let z = Math.floor(this.position[1] / cellSize); - placement.addInfluence(x, z, 255); - } - else // No position was specified so try and find a sensible place to build - { - // give a small > 0 level as the result of addInfluence is constrained to be > 0 - // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] == base) - placement.set(j, 45); - } - else - { - for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] != 0) - placement.set(j, 45); - } - - if (!HQ.requireHouses || !template.hasClass("House")) - { - gameState.getOwnStructures().forEach(function(ent) { - let pos = ent.position(); - let x = Math.round(pos[0] / cellSize); - let z = Math.round(pos[1] / cellSize); - - let struct = m.getBuiltEntity(gameState, ent); - if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) - { - if (template.hasClass("Field") || template.hasClass("Corral")) - placement.addInfluence(x, z, 80/cellSize, 50); - else // If this is not a field add a negative influence because we want to leave this area for fields - placement.addInfluence(x, z, 80/cellSize, -20); - } - else if (template.hasClass("House")) - { - if (ent.hasClass("House")) - { - placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses - alreadyHasHouses = true; - } - else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) - placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs - } - else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && - (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) - placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) - else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) - placement.addInfluence(x, z, 120/cellSize, -50); - else if (template.hasClass("Military")) - placement.addInfluence(x, z, 40/cellSize, -40); - else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) - placement.addInfluence(x, z, 60/cellSize, 40); - }); - } - if (template.hasClass("Farmstead")) - { - for (let j = 0; j < placement.map.length; ++j) - { - let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; - if (HQ.borderMap.map[j] & m.fullBorder_Mask) - value /= 2; // we need space around farmstead, so disfavor map border - placement.set(j, value); - } - } - } - - // Requires to be inside our territory, and inside our base territory if required - // and if our first market, put it on border if possible to maximize distance with next market - let favorBorder = template.hasClass("BarterMarket"); - let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); - let favoredBase = this.metadata && (this.metadata.favoredBase || - (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); - if (this.metadata && this.metadata.base !== undefined) - { - let base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] != base) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - } - } - } - else - { - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.basesMap.map[j] == 0) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) - placement.set(j, placement.map[j] + 10); - - let x = (j % placement.width + 0.5) * cellSize; - let z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - else if (favoredBase && HQ.basesMap.map[j] == favoredBase) - placement.set(j, placement.map[j] + 100); - } - } - } - - // Find the best non-obstructed: - // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, - // this allows room for units to walk between buildings. - // note: not for houses and dropsites who ought to be closer to either each other or a resource. - // also not for fields who can be stacked quite a bit - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let radius = 0; - if (template.hasClass("Fortress") || template.hasClass("Workshop") || - this.type == gameState.applyCiv("structures/{civ}_elephant_stables")) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && - !template.hasClass("Field") && !template.hasClass("BarterMarket")) - radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let bestTile; - if (template.hasClass("House") && !alreadyHasHouses) - { - // try to get some space to place several houses first - bestTile = placement.findBestTile(3*radius, obstructions); - if (!bestTile.val) - bestTile = undefined; - } - - if (!bestTile) - bestTile = placement.findBestTile(radius, obstructions); - - if (!bestTile.val) - return false; - - let bestIdx = bestTile.idx; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - let territorypos = placement.gamePosToMapPos([x, z]); - let territoryIndex = territorypos[0] + territorypos[1]*placement.width; - // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; -}; - -/** - * Placement of buildings with Dock build category - * metadata.proximity is defined when first dock without any territory - * => we try to minimize distance from our current point - * metadata.oversea is defined for dock in oversea islands - * => we try to maximize distance to our current docks (for trade) - * otherwise standard dock on an island where we already have a cc - * => we try not to be too far from our territory - * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. - */ -m.ConstructionPlan.prototype.findDockPosition = function(gameState) -{ - let template = this.template; - let territoryMap = gameState.ai.HQ.territoryMap; - - let obstructions = m.createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let bestIdx; - let bestJdx; - let bestAngle; - let bestLand; - let bestWater; - let bestVal = -1; - let navalPassMap = gameState.ai.accessibility.navalPassMap; - - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - - let nbShips = gameState.ai.HQ.navalManager.transportShips.length; - let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; - let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; - let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; - let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; - if (nbShips == 0 && proxyAccess && proxyAccess > 1) - { - wantedLand = {}; - wantedLand[proxyAccess] = true; - } - let dropsiteTypes = template.resourceDropsiteTypes(); - let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let halfSize = 0; // used for dock angle - let halfDepth = 0; // used by checkPlacement - let halfWidth = 0; // used by checkPlacement - if (template.get("Footprint/Square")) - { - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - halfDepth = +template.get("Footprint/Square/@depth") / 2; - halfWidth = +template.get("Footprint/Square/@width") / 2; - } - else if (template.get("Footprint/Circle")) - { - halfSize = +template.get("Footprint/Circle/@radius"); - halfDepth = halfSize; - halfWidth = halfSize; - } - - // res is a measure of the amount of resources around, and maxRes is the max value taken into account - // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement - const maxRes = 10; - const maxWater = 16; - let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; - let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; - // Normalisation factors (only guessed, no attempt to optimize them) - let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; - for (let j = 0; j < territoryMap.length; ++j) - { - if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) - continue; - let score = 0; - if (!proxyAccess && !oversea) - { - // if not in our (or allied) territory, we do not want it too far to be able to defend it - score = this.getFrontierProximity(gameState, j); - if (score > 4) - continue; - score *= factor; - } - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - if (wantedSea && navalPassMap[i] != wantedSea) - continue; - - let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - // If proximity is given, we look for the nearest point - if (proxyAccess) - score = API3.VectorDistance(this.metadata.proximity, pos); - - // Bonus for resources - score += 20 * (maxRes - res); - - if (oversea) - { - // Not much farther to one of our cc than to enemy ones - let enemyDist; - let ownDist; - for (let cc of ccEnts.values()) - { - let owner = cc.owner(); - if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) - continue; - let dist = API3.SquareVectorDistance(pos, cc.position()); - if (owner == PlayerID && (!ownDist || dist < ownDist)) - ownDist = dist; - if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) - enemyDist = dist; - } - if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) - continue; - - // And maximize distance for trade. - let dockDist = 0; - for (let dock of docks.values()) - { - if (m.getSeaAccess(gameState, dock) != navalPassMap[i]) - continue; - let dist = API3.SquareVectorDistance(pos, dock.position()); - if (dist > dockDist) - dockDist = dist; - } - if (dockDist > 0) - { - dockDist = Math.sqrt(dockDist); - if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away - continue; - score += factor * (width * cellSize - dockDist); - } - } - - // Add a penalty if on the map border as ship movement will be difficult - if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) - score += 20; - - // Do a pre-selection, supposing we will have the best possible water - if (bestIdx !== undefined && score > bestVal + 5 * maxWater) - continue; - - let x = (i % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; - let angle = this.getDockAngle(gameState, x, z, halfSize); - if (angle == false) - continue; - let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); - if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) - continue; - // Final selection now that the checkDockPlacement water is known - if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) - continue; - if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - - bestVal = score + maxWater - ret.water; - bestIdx = i; - bestJdx = j; - bestAngle = angle; - bestLand = ret.land; - bestWater = ret.water; - } - if (bestVal < 0) - return false; - - // if no good place with enough water around and still in first phase, wait for expansion at the next phase - if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) - return false; - - let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Assign this dock to a base - let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; - if (!baseIndex) - baseIndex = -2; // We'll do an anchorless base around it - - return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; -}; - -/** - * Find a good island to build a dock. - */ -m.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) -{ - let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); - if (!docks.hasEntities()) - return; - - let passabilityMap = gameState.getPassabilityMap(); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); - - let land = {}; - let found; - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (gameState.ai.accessibility.regionType[i] != "land" || - cellArea * gameState.ai.accessibility.regionSize[i] < 3600) - continue; - let keep = true; - for (let dock of docks.values()) - { - if (m.getLandAccess(gameState, dock) != i) - continue; - keep = false; - break; - } - if (!keep) - continue; - let sea; - for (let cc of ccEnts.values()) - { - let ccAccess = m.getLandAccess(gameState, cc); - if (ccAccess != i) - { - if (cc.owner() == PlayerID && !sea) - sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); - continue; - } - // Docks on island where we have a cc are already done elsewhere - if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) - { - keep = false; - break; - } - } - if (!keep || !sea) - continue; - land[i] = true; - found = true; - } - if (!found) - return; - if (!gameState.ai.HQ.navalMap) - API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); - - let oldTemplate = this.template; - let oldMetadata = this.metadata; - this.template = template; - let pos; - this.metadata = { "land": land, "oversea": true }; - pos = this.findDockPosition(gameState); - if (pos) - { - let type = template.templateName(); - let builder = gameState.findBuilder(type); - this.metadata.base = pos.base; - // Adjust a bit the position if needed - let cosa = Math.cos(pos.angle); - let sina = Math.sin(pos.angle); - let shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) - { - builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); - } - } - this.template = oldTemplate; - this.metadata = oldMetadata; -}; - -/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ -m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) -{ - let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*gameState.ai.accessibility.width; - let seaRef = gameState.ai.accessibility.navalPassMap[k]; - if (seaRef < 2) - return false; - const numPoints = 16; - for (let dist = 0; dist < 4; ++dist) - { - let waterPoints = []; - for (let i = 0; i < numPoints; ++i) - { - let angle = 2 * Math.PI * i / numPoints; - pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; - pos = gameState.ai.accessibility.gamePosToMapPos(pos); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - continue; - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.navalPassMap[j] == seaRef) - waterPoints.push(i); - } - let length = waterPoints.length; - if (!length) - continue; - let consec = []; - for (let i = 0; i < length; ++i) - { - let count = 0; - for (let j = 0; j < length-1; ++j) - { - if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) - ++count; - else - break; - } - consec[i] = count; - } - let start = 0; - let count = 0; - for (let c in consec) - { - if (consec[c] > count) - { - start = c; - count = consec[c]; - } - } - - // If we've found a shoreline, stop searching - if (count != numPoints-1) - return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; - } - return false; -}; - -/** - * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js - * to determine the special dock requirements - * returns {"land": land index for this dock, "water": amount of water around this spot} - */ -m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) -{ - let sz = halfDepth * Math.sin(angle); - let cz = halfDepth * Math.cos(angle); - // center back position - let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[j]; - if (land < 2) - return null; - // center front position - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - return null; - // additional constraints compared to BuildRestriction.js to assure we have enough place to build - let sw = halfWidth * Math.cos(angle) * 3 / 4; - let cw = halfWidth * Math.sin(angle) * 3 / 4; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - let water = 0; - let sp = 15 * Math.sin(angle); - let cp = 15 * Math.cos(angle); - for (let i = 1; i < 5; ++i) - { - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - water += 4; - } - return { "land": land, "water": water }; -}; - -/** - * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension - * if the (object) wantedLand is given, this nearest land should have one of these accessibility - * if wantedSea is given, this tile should be inside this sea - */ -const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], - [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50]]; - -m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) -{ - let width = gameState.ai.HQ.territoryMap.width; - let cellSize = gameState.ai.HQ.territoryMap.cellSize; - let dimLand = dimension + 1.5 * cellSize; - let dimSea = dimension + 2 * cellSize; - - let accessibility = gameState.ai.accessibility; - let x = (j%width + 0.5) * cellSize; - let z = (Math.floor(j/width) + 0.5) * cellSize; - let pos = accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*accessibility.width; - let landPass = accessibility.landPassMap[k]; - if (landPass > 1 && wantedLand && !wantedLand[landPass] || - landPass < 2 && accessibility.navalPassMap[k] < 2) - return false; - - for (let a of around) - { - pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - landPass = accessibility.landPassMap[k]; - if (landPass < 2 || wantedLand && !wantedLand[landPass]) - continue; - pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - if (wantedSea && accessibility.navalPassMap[k] != wantedSea || - !wantedSea && accessibility.navalPassMap[k] < 2) - continue; - return true; - } - - return false; -}; - -/** - * return a measure of the proximity to our frontier (including our allies) - * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m - */ -m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) -{ - let alliedVictory = gameState.getAlliedVictory(); - let territoryMap = gameState.ai.HQ.territoryMap; - let territoryOwner = territoryMap.getOwnerIndex(j); - if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) - return 0; - - let borderMap = gameState.ai.HQ.borderMap; - let width = territoryMap.width; - let step = Math.round(24 / territoryMap.cellSize); - let ix = j % width; - let iz = Math.floor(j / width); - let best = 5; - for (let a of around) - { - for (let i = 1; i < 5; ++i) - { - let jx = ix + Math.round(i*step*a[0]); - if (jx < 0 || jx >= width) - continue; - let jz = iz + Math.round(i*step*a[1]); - if (jz < 0 || jz >= width) - continue; - if (borderMap.map[jx+width*jz] & m.outside_Mask) - continue; - territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); - if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) - { - best = Math.min(best, i); - break; - } - } - if (best == 1) - break; - } - - return best; -}; - -/** - * get the sum of the resources (except food) around, inside a given radius - * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood - */ -m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) -{ - let resourceMaps = gameState.sharedScript.resourceMaps; - let w = resourceMaps.wood.width; - let cellSize = resourceMaps.wood.cellSize; - let size = Math.floor(radius / cellSize); - let ix = i % w; - let iy = Math.floor(i / w); - let total = 0; - let nbcell = 0; - for (let k of types) - { - if (k == "food" || !resourceMaps[k]) - continue; - let weigh0 = k == "wood" ? 2 : 1; - for (let dy = 0; dy <= size; ++dy) - { - let dxmax = size - dy; - let ky = iy + dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - if (dy == 0) - continue; - ky = iy - dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - let kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - let ddx = dx > 0 ? dx : -dx; - let weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - } - } - return nbcell ? total / nbcell : 0; -}; - -m.ConstructionPlan.prototype.isGo = function(gameState) -{ - if (this.goRequirement && this.goRequirement == "houseNeeded") - { - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) - return false; - if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return false; - let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); - for (let ent of gameState.getOwnFoundations().values()) - { - let template = gameState.getBuiltTemplate(ent.templateName()); - if (template) - freeSlots += template.getPopulationBonus(); - } - - if (gameState.ai.HQ.saveResources) - return freeSlots <= 10; - if (gameState.getPopulation() > 55) - return freeSlots <= 21; - if (gameState.getPopulation() > 30) - return freeSlots <= 15; - return freeSlots <= 10; - } - return true; -}; - -m.ConstructionPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); -}; - -m.ConstructionPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "position": this.position, - "goRequirement": this.goRequirement || undefined, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ConstructionPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/queueplanResearch.js b/install/petraBased/petra-unitary/queueplanResearch.js deleted file mode 100644 index 2e09783..0000000 --- a/install/petraBased/petra-unitary/queueplanResearch.js +++ /dev/null @@ -1,113 +0,0 @@ -var PETRA = function(m) -{ - -m.ResearchPlan = function(gameState, type, rush = false) -{ - if (!m.QueuePlan.call(this, gameState, type, {})) - return false; - - if (this.template.researchTime === undefined) - return false; - - // Refine the estimated cost - let researchers = this.getBestResearchers(gameState, true); - if (researchers) - this.cost = new API3.Resources(this.template.cost(researchers[0])); - - this.category = "technology"; - this.rush = rush; - - return true; -}; - -m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.ResearchPlan.prototype.canStart = function(gameState) -{ - this.researchers = this.getBestResearchers(gameState); - if (!this.researchers) - return false; - this.cost = new API3.Resources(this.template.cost(this.researchers[0])); - return true; -}; - -m.ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false) -{ - let allResearchers = gameState.findResearchers(this.type, noRequirementCheck); - if (!allResearchers || !allResearchers.hasEntities()) - return undefined; - - // Keep only researchers with smallest cost - let costMin = Math.min(); - let researchers; - for (let ent of allResearchers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - researchers.push(ent); - else if (cost < costMin) - { - costMin = cost; - researchers = [ent]; - } - } - return researchers; -}; - -m.ResearchPlan.prototype.isInvalid = function(gameState) -{ - return gameState.isResearched(this.type) || gameState.isResearching(this.type); -}; - -m.ResearchPlan.prototype.start = function(gameState) -{ - // Prefer researcher with shortest queues (no need to serialize this.researchers - // as the functions canStart and start are always called on the same turn) - this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); - // Drop anything in the queue if we rush it. - if (this.rush) - this.researchers[0].stopAllProduction(0.45); - this.researchers[0].research(this.type); - this.onStart(gameState); -}; - -m.ResearchPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); - - for (let i = gameState.getNumberOfPhases(); i > 0; --i) - { - if (this.type != gameState.getPhaseName(i)) - continue; - gameState.ai.HQ.phasing = 0; - gameState.ai.HQ.OnPhaseUp(gameState, i); - break; - } -}; - -m.ResearchPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "rush": this.rush, - "queueToReset": this.queueToReset || undefined - }; -}; - -m.ResearchPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/queueplanTraining.js b/install/petraBased/petra-unitary/queueplanTraining.js deleted file mode 100644 index e63e387..0000000 --- a/install/petraBased/petra-unitary/queueplanTraining.js +++ /dev/null @@ -1,193 +0,0 @@ -var PETRA = function(m) -{ - -m.TrainingPlan = function(gameState, type, metadata, number = 1, maxMerge = 5) -{ - if (!m.QueuePlan.call(this, gameState, type, metadata)) - { - API3.warn(" Plan training " + type + " canceled"); - return false; - } - - // Refine the estimated cost and add pop cost - let trainers = this.getBestTrainers(gameState); - let trainer = trainers ? trainers[0] : undefined; - this.cost = new API3.Resources(this.template.cost(trainer), +this.template._template.Cost.Population); - - this.category = "unit"; - this.number = number; - this.maxMerge = maxMerge; - - return true; -}; - -m.TrainingPlan.prototype = Object.create(m.QueuePlan.prototype); - -m.TrainingPlan.prototype.canStart = function(gameState) -{ - this.trainers = this.getBestTrainers(gameState); - if (!this.trainers) - return false; - this.cost = new API3.Resources(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); - return true; -}; - -m.TrainingPlan.prototype.getBestTrainers = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let trainer = gameState.getEntityById(this.metadata.trainer); - if (trainer) - return [trainer]; - } - - let allTrainers = gameState.findTrainers(this.type); - if (this.metadata && this.metadata.sea) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)); - if (this.metadata && this.metadata.base) - allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "base", this.metadata.base)); - if (!allTrainers || !allTrainers.hasEntities()) - return undefined; - - // Keep only trainers with smallest cost - let costMin = Math.min(); - let trainers; - for (let ent of allTrainers.values()) - { - let cost = this.template.costSum(ent); - if (cost === costMin) - trainers.push(ent); - else if (cost < costMin) - { - costMin = cost; - trainers = [ent]; - } - } - return trainers; -}; - -m.TrainingPlan.prototype.start = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - let metadata = {}; - for (let key in this.metadata) - if (key !== "trainer") - metadata[key] = this.metadata[key]; - this.metadata = metadata; - } - - if (this.trainers.length > 1) - { - let wantedIndex; - if (this.metadata && this.metadata.index) - wantedIndex = this.metadata.index; - let workerUnit = this.metadata && this.metadata.role && this.metadata.role == "worker"; - let supportUnit = this.template.hasClass("Support"); - this.trainers.sort(function(a, b) { - // Prefer training buildings with short queues - let aa = a.trainingQueueTime(); - let bb = b.trainingQueueTime(); - // Give priority to support units in the cc - if (a.hasClass("Civic") && !supportUnit) - aa += 10; - if (b.hasClass("Civic") && !supportUnit) - bb += 10; - // And support units should not be too near to dangerous place - if (supportUnit) - { - if (gameState.ai.HQ.isNearInvadingArmy(a.position())) - aa += 50; - if (gameState.ai.HQ.isNearInvadingArmy(b.position())) - bb += 50; - } - // Give also priority to buildings with the right accessibility - let aBase = a.getMetadata(PlayerID, "base"); - let bBase = b.getMetadata(PlayerID, "base"); - if (wantedIndex) - { - if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) - aa += 30; - if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) - bb += 30; - } - // Then, if workers, small preference for bases with less workers - if (workerUnit && aBase && bBase && aBase != bBase) - { - let apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; - let bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; - if (apop > bpop) - aa++; - else if (bpop > apop) - bb++; - } - return aa - bb; - }); - } - - if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) - this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); - this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState)); - - this.onStart(gameState); -}; - -m.TrainingPlan.prototype.addItem = function(amount = 1) -{ - this.number += amount; -}; - -/** Find the promoted types corresponding to this.type */ -m.TrainingPlan.prototype.promotedTypes = function(gameState) -{ - let types = []; - let promotion = this.template.promotion(); - let previous; - let template; - while (promotion) - { - types.push(promotion); - previous = promotion; - template = gameState.getTemplate(promotion); - if (!template) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" promotion template " + promotion + " is not found"); - promotion = undefined; - break; - } - promotion = template.promotion(); - if (previous === promotion) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" unit " + promotion + " is its own promoted unit"); - promotion = undefined; - } - } - return types; -}; - -m.TrainingPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "maxMerge": this.maxMerge - }; -}; - -m.TrainingPlan.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - this[key] = data[key]; - - this.cost = new API3.Resources(); - this.cost.Deserialize(data.cost); -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/researchManager.js b/install/petraBased/petra-unitary/researchManager.js deleted file mode 100644 index 51ce3ff..0000000 --- a/install/petraBased/petra-unitary/researchManager.js +++ /dev/null @@ -1,244 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the research - */ - -m.ResearchManager = function(Config) -{ - this.Config = Config; -}; - -/** - * Check if we can go to the next phase - */ -m.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; - - let currentPhaseIndex = gameState.currentPhase(); - let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); - if (!nextPhaseName) - return; - - let 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 m.ResearchPlan(gameState, nextPhaseName, true)); - } -}; - -m.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 !== "Cost/PopulationBonus") - continue; - queues.minorTech.addPlan(new m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -m.ResearchManager.prototype.researchTradeBonus = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - for (let 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 m.ResearchPlan(gameState, tech[0])); - break; - } -}; - -/** Techs to be searched for as soon as they are available */ -m.ResearchManager.prototype.researchWantedTechs = function(gameState, techs) -{ - let phase1 = gameState.currentPhase() === 1; - let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase1) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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 */ -m.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) -{ - let phase2 = gameState.currentPhase() === 2; - let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; - let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; - for (let tech of techs) - { - if (!tech[1]._template.modifications) - continue; - let template = tech[1]._template; - if (phase2) - { - let cost = template.cost; - let costMax = 0; - for (let res in cost) - costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); - if (10*numWorkers < costMax) - continue; - } - for (let 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; -}; - -m.ResearchManager.prototype.update = function(gameState, queues) -{ - if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) - return; - - let techs = gameState.findAvailableTech(); - - let techName = this.researchWantedTechs(gameState, techs); - if (techName) - { - if (techName.increasePriority) - { - gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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); - let plan = new m.ResearchPlan(gameState, techName.name); - plan.queueToReset = "minorTech"; - queues.minorTech.addPlan(plan); - } - else - queues.minorTech.addPlan(new m.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) - { - let template = techs[i][1]._template; - if (template.affects && template.affects.length === 1 && - (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "StoneWall")) - { - techs.splice(i--, 1); - continue; - } - if (template.modifications && template.modifications.length === 1 && - template.modifications[0].value === "Player/sharedLos" && - !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 m.ResearchPlan(gameState, pickRandom(techs)[0])); -}; - -m.ResearchManager.prototype.CostSum = function(cost) -{ - let costSum = 0; - for (let res in cost) - costSum += cost[res]; - return costSum; -}; - -m.ResearchManager.prototype.Serialize = function() -{ - return {}; -}; - -m.ResearchManager.prototype.Deserialize = function(data) -{ -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/startingStrategy.js b/install/petraBased/petra-unitary/startingStrategy.js deleted file mode 100644 index db76f30..0000000 --- a/install/petraBased/petra-unitary/startingStrategy.js +++ /dev/null @@ -1,578 +0,0 @@ -var PETRA = function(m) -{ -/** - * determines the strategy to adopt when starting a new game, depending on the initial conditions - */ - -m.HQ.prototype.gameAnalysis = function(gameState) -{ - // Analysis of the terrain and the different access regions - if (!this.regionAnalysis(gameState)) - return; - - this.attackManager.init(gameState); - this.buildManager.init(gameState); - this.navalManager.init(gameState); - this.tradeManager.init(gameState); - this.diplomacyManager.init(gameState); - - // Make a list of buildable structures from the config file - this.structureAnalysis(gameState); - - // Let's get our initial situation here. - let nobase = new m.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); - this.updateTerritories(gameState); - - // Assign entities and resources in the different bases - this.assignStartingEntities(gameState); - - - // Sandbox difficulty should not try to expand - this.canExpand = this.Config.difficulty != 0; - // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks - this.canBuildUnits = true; - if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) - { - let template = gameState.applyCiv("structures/{civ}_civil_centre"); - if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) - { - if (this.Config.debug > 1) - API3.warn(" this AI is unable to produce any units"); - this.canBuildUnits = false; - this.dispatchUnits(gameState); - } - else - this.buildFirstBase(gameState); - } - - // configure our first base strategy - if (this.baseManagers.length > 1) - this.configFirstBase(gameState); -}; - -/** - * Assign the starting entities to the different bases - */ -m.HQ.prototype.assignStartingEntities = function(gameState) -{ - for (let ent of gameState.getOwnEntities().values()) - { - // do not affect merchant ship immediately to trade as they may-be useful for transport - if (ent.hasClass("Trader") && !ent.hasClass("Ship")) - this.tradeManager.assignTrader(ent); - - let pos = ent.position(); - if (!pos) - { - // TODO should support recursive garrisoning. Make a warning for now - if (ent.isGarrisonHolder() && ent.garrisoned().length) - API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); - continue; - } - - // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - if (land > 1 && !this.landRegions[land]) - this.landRegions[land] = true; - let sea = gameState.ai.accessibility.navalPassMap[index]; - if (sea > 1 && !this.navalRegions[sea]) - this.navalRegions[sea] = true; - - // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport - // when a construction will start (see createTransportIfNeeded) - if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) - for (let id of ent.garrisoned()) - ent.unload(id); - - let bestbase; - let territorypos = this.territoryMap.gamePosToMapPos(pos); - let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; - for (let i = 1; i < this.baseManagers.length; ++i) - { - let base = this.baseManagers[i]; - 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, "anchorless"); - else - bestbase = m.getBestBase(gameState, ent) || this.baseManagers[0]; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (pos && bestbase.ID !== this.baseManagers[0].ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === "worker") - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } - } -}; - -/** - * determine the main land Index (or water index if none) - * as well as the list of allowed (land andf water) regions - */ -m.HQ.prototype.regionAnalysis = function(gameState) -{ - let accessibility = gameState.ai.accessibility; - let landIndex; - let seaIndex; - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - let land = accessibility.getAccessValue(cc.position()); - if (land > 1) - { - landIndex = land; - break; - } - } - if (!landIndex) - { - let civ = gameState.getPlayerCiv(); - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) - continue; - let land = accessibility.getAccessValue(ent.position()); - if (land > 1) - { - landIndex = land; - break; - } - let sea = accessibility.getAccessValue(ent.position(), true); - if (!seaIndex && sea > 1) - seaIndex = sea; - } - } - if (!landIndex && !seaIndex) - { - API3.warn("Petra error: it does not know how to interpret this map"); - return false; - } - - let passabilityMap = gameState.getPassabilityMap(); - let totalSize = passabilityMap.width * passabilityMap.width; - let minLandSize = Math.floor(0.1*totalSize); - let minWaterSize = Math.floor(0.2*totalSize); - let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - for (let i = 0; i < accessibility.regionSize.length; ++i) - { - if (landIndex && i == landIndex) - this.landRegions[i] = true; - else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) - { - if (landIndex) - { - let sea = this.getSeaBetweenIndices(gameState, landIndex, i); - if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[sea] = true; - } - } - else - { - let traject = accessibility.getTrajectToIndex(seaIndex, i); - if (traject && traject.length === 2) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[seaIndex] = true; - } - } - } - else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) - { - this.navalMap = true; - this.navalRegions[i] = true; - } - else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) - this.navalRegions[i] = true; - } - - if (this.Config.debug < 3) - return true; - for (let region in this.landRegions) - API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); - API3.warn(" navalMap " + this.navalMap); - API3.warn(" landRegions " + uneval(this.landRegions)); - API3.warn(" navalRegions " + uneval(this.navalRegions)); - return true; -}; - -/** - * load units and buildings from the config files - * TODO: change that to something dynamic - */ -m.HQ.prototype.structureAnalysis = function(gameState) -{ - let civref = gameState.playerData.civ; - let civ = civref in this.Config.buildings ? civref : 'default'; - this.bAdvanced = []; - for (let building of this.Config.buildings[civ]) - if (gameState.isTemplateAvailable(gameState.applyCiv(building))) - this.bAdvanced.push(gameState.applyCiv(building)); -}; - -/** - * build our first base - * if not enough resource, try first to do a dock - */ -m.HQ.prototype.buildFirstBase = function(gameState) -{ - if (gameState.ai.queues.civilCentre.hasQueuedUnits()) - return; - let templateName = gameState.applyCiv("structures/{civ}_civil_centre"); - if (gameState.isTemplateDisabled(templateName)) - return; - let template = gameState.getTemplate(templateName); - if (!template) - return; - let total = gameState.getResources(); - let goal = "civil_centre"; - if (!total.canAfford(new API3.Resources(template.cost()))) - { - let totalExpected = gameState.getResources(); - // Check for treasures around available in some maps at startup - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position()) - continue; - // If we can get a treasure around, just do it - if (ent.isIdle()) - m.gatherTreasure(gameState, ent); - // Then count the resources from the treasures being collected - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (!supplyId) - continue; - let supply = gameState.getEntityById(supplyId); - if (!supply || supply.resourceSupplyType().generic != "treasure") - continue; - let type = supply.resourceSupplyType().specific; - if (!(type in totalExpected)) - continue; - totalExpected[type] += supply.resourceSupplyMax(); - // If we can collect enough resources from these treasures, wait for them - if (totalExpected.canAfford(new API3.Resources(template.cost()))) - return; - } - - // not enough resource to build a cc, try with a dock to accumulate resources if none yet - if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) - { - if (gameState.ai.queues.dock.hasQueuedUnits()) - return; - templateName = gameState.applyCiv("structures/{civ}_dock"); - if (gameState.isTemplateDisabled(templateName)) - return; - template = gameState.getTemplate(templateName); - if (!template || !total.canAfford(new API3.Resources(template.cost()))) - return; - goal = "dock"; - } - } - if (!this.canBuild(gameState, templateName)) - return; - - // We first choose as startingPoint the point where we have the more units - let startingPoint = []; - for (let ent of gameState.getOwnUnits().values()) - { - if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) - continue; - if (ent.hasClass("Cavalry")) - continue; - let pos = ent.position(); - if (!pos) - { - let holder = m.getHolder(gameState, ent); - if (!holder || !holder.position()) - continue; - pos = holder.position(); - } - let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); - let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - let sea = gameState.ai.accessibility.navalPassMap[index]; - let found = false; - for (let point of startingPoint) - { - if (land !== point.land || sea !== point.sea) - continue; - if (API3.SquareVectorDistance(point.pos, pos) > 2500) - continue; - point.weight += 1; - found = true; - break; - } - if (!found) - startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 }); - } - if (!startingPoint.length) - return; - - let imax = 0; - for (let i = 1; i < startingPoint.length; ++i) - if (startingPoint[i].weight > startingPoint[imax].weight) - imax = i; - - if (goal == "dock") - { - let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; - gameState.ai.queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); - } - else - gameState.ai.queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); -}; - -/** - * set strategy if game without construction: - * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack - * - otherwise all units will attack - */ -m.HQ.prototype.dispatchUnits = function(gameState) -{ - let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - if (allycc.length) - { - if (this.Config.debug > 1) - API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); - let units = gameState.getOwnUnits(); - let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); - let num1 = Math.floor(num / 2); - let num2 = num1; - // first pass to affect ranged infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => { - if (!num || !num1) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num1; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // second pass to affect melee infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => { - if (!num || !num2) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - --num; - --num2; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // and now complete the affectation, including all support units - units.forEach(ent => { - if (!num && !ent.hasClass("Support")) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - let access = m.getLandAccess(gameState, ent); - for (let cc of allycc) - { - if (!cc.position() || m.getLandAccess(gameState, cc) != access) - continue; - if (!ent.hasClass("Support")) - --num; - ent.setMetadata(PlayerID, "allied", true); - let range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - } -}; - -/** - * configure our first base expansion - * - if on a small island, favor fishing - * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - */ -m.HQ.prototype.configFirstBase = function(gameState) -{ - if (this.baseManagers.length < 2) - return; - - this.firstBaseConfig = true; - - let startingSize = 0; - let startingLand = []; - for (let region in this.landRegions) - { - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex != +region) - continue; - startingSize += gameState.ai.accessibility.regionSize[region]; - startingLand.push(base.accessIndex); - break; - } - } - let cell = gameState.getPassabilityMap().cellSize; - startingSize = startingSize * cell * cell; - if (this.Config.debug > 1) - API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); - if (startingSize < 25000) - { - this.saveSpace = true; - this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); - let num = Math.max(this.Config.Economy.targetNumFishers, 2); - for (let land of startingLand) - { - for (let sea of gameState.ai.accessibility.regionLinks[land]) - if (gameState.ai.HQ.navalRegions[sea]) - this.navalManager.updateFishingBoats(sea, num); - } - this.maxFields = 1; - this.needCorral = true; - } - else if (startingSize < 60000) - this.maxFields = 2; - else - this.maxFields = false; - - // - count the available wood resource, and react accordingly - let startingFood = gameState.getResources().food; - let check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.food[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingFood += supply.ent.resourceSupplyAmount(); - } - } - } - if (startingFood < 800) - { - if (startingSize < 25000) - { - this.needFish = true; - this.Config.Economy.popForDock = 1; - } - else - this.needFarm = true; - } - // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - let startingWood = gameState.getResources().wood; - check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.wood[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingWood += supply.ent.resourceSupplyAmount(); - } - } - } - if (this.Config.debug > 1) - API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); - if (startingWood < 6000) - { - this.saveResources = true; - this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand - - if (startingWood < 2000 && this.needFarm) - { - this.needCorral = true; - this.needFarm = false; - } - } - if (startingWood > 8500 && this.canBuildUnits) - { - let allowed = Math.ceil((startingWood - 8500) / 3000); - // Not useful to prepare rushing if too long ceasefire - if (gameState.isCeasefireActive()) - { - if (gameState.ceasefireTimeRemaining > 900) - allowed = 0; - else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) - allowed = 1; - } - this.attackManager.setRushes(allowed); - } - - // immediatly build a wood dropsite if possible. - let template = gameState.applyCiv("structures/{civ}_storehouse"); - if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) - { - let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); - if (newDP.quality > 40) - { - // if we start with enough workers, put our available resources in this first dropsite - // same thing if our pop exceed the allowed one, as we will need several houses - let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; - if (numWorkers > 12 && newDP.quality > 60 || - gameState.getPopulation() > gameState.getPopulationLimit() + 20) - { - let cost = new API3.Resources(gameState.getTemplate(template).cost()); - gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); - } - gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); - } - } - // and build immediately a corral if needed - if (this.needCorral) - { - template = gameState.applyCiv("structures/{civ}_corral"); - if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) - gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); - } -}; - -return m; - -}(PETRA); diff --git a/install/petraBased/petra-unitary/tradeManager.js b/install/petraBased/petra-unitary/tradeManager.js deleted file mode 100644 index 0878f6c..0000000 --- a/install/petraBased/petra-unitary/tradeManager.js +++ /dev/null @@ -1,723 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Manage the trade - */ - -m.TradeManager = function(Config) -{ - this.Config = Config; - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - this.routeProspection = false; - this.targetNumTraders = this.Config.Economy.targetNumTraders; - this.warnedAllies = {}; -}; - -m.TradeManager.prototype.init = function(gameState) -{ - this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); - this.traders.registerUpdates(); - this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; -}; - -m.TradeManager.prototype.hasTradeRoute = function() -{ - return this.tradeRoute !== undefined; -}; - -m.TradeManager.prototype.assignTrader = function(ent) -{ - ent.setMetadata(PlayerID, "role", "trader"); - this.traders.updateEnt(ent); -}; - -m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) -{ - if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) - return; - - let numTraders = this.traders.length; - let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length; - let numLandTraders = numTraders - numSeaTraders; - // add traders already in training - gameState.getOwnTrainingFacilities().forEach(function(ent) { - for (let item of ent.trainingQueue()) - { - if (!item.metadata || !item.metadata.role || item.metadata.role != "trader") - continue; - numTraders += item.count; - if (item.metadata.sea !== undefined) - numSeaTraders += item.count; - else - numLandTraders += item.count; - } - }); - if (numTraders >= this.targetNumTraders && - (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || - this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) - return; - - let template; - let metadata = { "role": "trader" }; - if (this.tradeRoute.sea) - { - // if we have some merchand ships affected to transport, try first to reaffect them - // May-be, there were produced at an early stage when no other ship were available - // and the naval manager will train now more appropriate ships. - let already = false; - let shipToSwitch; - gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) { - if (already || !ship.hasClass("Trader")) - return; - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - { - already = true; - return; - } - shipToSwitch = ship; - }); - if (already) - return; - if (shipToSwitch) - { - if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) - shipToSwitch.setMetadata(PlayerID, "role", "trader"); - else - shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader"); - return; - } - - template = gameState.applyCiv("units/{civ}_ship_merchant"); - metadata.sea = this.tradeRoute.sea; - } - else - { - template = gameState.applyCiv("units/{civ}_support_trader"); - if (!this.tradeRoute.source.hasClass("NavalMarket")) - metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base"); - else - metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base"); - } - - if (!gameState.getTemplate(template)) - { - if (this.Config.debug > 0) - API3.warn("Petra error: trying to train " + template + " for civ " + - gameState.getPlayerCiv() + " but no template found."); - return; - } - queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1)); -}; - -m.TradeManager.prototype.updateTrader = function(gameState, ent) -{ - if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && - !ent.unitAIState().startsWith("INDIVIDUAL.GATHER") && - m.gatherTreasure(gameState, ent, true)) - return; - - if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) - return; - if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; - - // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain - - Engine.ProfileStart("Trade Manager"); - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let route = this.checkRoutes(gameState, access); - if (!route) - { - // TODO try to garrison land trader inside merchant ship when only sea routes available - if (this.Config.debug > 0) - API3.warn(" no available route for " + ent.genericName() + " " + ent.id()); - Engine.ProfileStop(); - return; - } - - let nearerSource = true; - if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position())) - nearerSource = false; - - if (!ent.hasClass("Ship") && route.land != access) - { - if (nearerSource) - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position()); - else - gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position()); - Engine.ProfileStop(); - return; - } - - if (nearerSource) - ent.tradeRoute(route.target, route.source); - else - ent.tradeRoute(route.source, route.target); - ent.setMetadata(PlayerID, "route", this.routeEntToId(route)); - Engine.ProfileStop(); -}; - -m.TradeManager.prototype.setTradingGoods = function(gameState) -{ - let tradingGoods = {}; - for (let res of Resources.GetCodes()) - tradingGoods[res] = 0; - // first, try to anticipate future needs - let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); - let remaining = 100; - let targetNum = this.Config.Economy.targetNumTraders; - for (let res in stocks) - { - if (res == "food") - continue; - let wantedRate = wantedRates[res]; - if (stocks[res] < 200) - { - tradingGoods[res] = wantedRate > 0 ? 20 : 10; - targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); - } - else if (stocks[res] < 500) - { - tradingGoods[res] = wantedRate > 0 ? 15 : 10; - targetNum += 2; - } - else if (stocks[res] < 1000) - { - tradingGoods[res] = 10; - targetNum += 1; - } - remaining -= tradingGoods[res]; - } - this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); - - - // then add what is needed now - let mainNeed = Math.floor(remaining * 70 / 100); - let nextNeed = remaining - mainNeed; - - tradingGoods[mostNeeded[0].type] += mainNeed; - if (mostNeeded[1].wanted > 0) - tradingGoods[mostNeeded[1].type] += nextNeed; - else - tradingGoods[mostNeeded[0].type] += nextNeed; - Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); - if (this.Config.debug > 2) - API3.warn(" trading goods set to " + uneval(tradingGoods)); -}; - -/** - * Try to barter unneeded resources for needed resources. - * only once per turn because the info is not updated within a turn - */ -m.TradeManager.prototype.performBarter = function(gameState) -{ - let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); - if (barterers.length == 0) - return false; - - // Available resources after account substraction - let available = gameState.ai.queueManager.getAvailableResources(gameState); - let needs = gameState.ai.queueManager.currentNeeds(gameState); - - let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); - - let barterPrices = gameState.getBarterPrices(); - // calculates conversion rates - let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); - - // loop through each missing resource checking if we could barter and help finishing a queue quickly. - for (let buy of Resources.GetCodes()) - { - // 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 (let sell of Resources.GetCodes()) - { - 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; - } - - let barterRate = getBarterRate(barterPrices, buy, sell); - if (barterRate > bestRate && barterRate > barterRateMin) - { - bestRate = barterRate; - bestToSell = sell; - } - } - if (bestToSell !== undefined) - { - let amount = available[bestToSell] > 5000 ? 500 : 100; - barterers[0].barter(buy, bestToSell, amount); - if (this.Config.debug > 2) - API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + - " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + - " rate buy " + rates[buy] + " available sell " + available[bestToSell] + - " available buy " + available[buy] + " barterRate " + bestRate + - " 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) - return false; - let bestToBuy; - let bestChoice = 0; - for (let buy of Resources.GetCodes()) - { - if (buy == "food") - continue; - let barterRateMin = 80; - if (available[buy] < 5000 && available.food > 5000) - barterRateMin -= 20 - Math.floor(available[buy]/250); - let barterRate = getBarterRate(barterPrices, buy, "food"); - if (barterRate < barterRateMin) - continue; - let choice = barterRate / (100 + available[buy]); - if (choice > bestChoice) - { - bestChoice = choice; - bestToBuy = buy; - } - } - if (bestToBuy !== undefined) - { - let amount = available.food > 5000 ? 500 : 100; - barterers[0].barter(bestToBuy, "food", amount); - if (this.Config.debug > 2) - API3.warn("Contingency bartering: sold food for " + bestToBuy + - " available sell " + available.food + " available buy " + available[bestToBuy] + - " barterRate " + getBarterRate(barterPrices, bestToBuy, "food") + - " amount " + amount); - return true; - } - - return false; -}; - -m.TradeManager.prototype.checkEvents = function(gameState, events) -{ - // check if one market from a traderoute is renamed, change the route accordingly - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || !ent.hasClass("Market")) - continue; - for (let trader of this.traders.values()) - { - let route = trader.getMetadata(PlayerID, "route"); - if (!route) - continue; - if (route.source == evt.entity) - route.source = evt.newentity; - else if (route.target == evt.entity) - route.target = evt.newentity; - else - continue; - trader.setMetadata(PlayerID, "route", route); - } - } - - // if one market (or market-foundation) is destroyed, we should look for a better route - for (let evt of events.Destroy) - { - if (!evt.entityObj) - continue; - let ent = evt.entityObj; - if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - // same thing if one market is built - for (let evt of events.Create) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || - !gameState.isPlayerAlly(ent.owner())) - continue; - this.activateProspection(gameState); - return true; - } - - - // and same thing for captured markets - for (let evt of events.OwnershipChanged) - { - if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) - continue; - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) - continue; - this.activateProspection(gameState); - return true; - } - - // or if diplomacy changed - if (events.DiplomacyChanged.length) - { - this.activateProspection(gameState); - return true; - } - - return false; -}; - -m.TradeManager.prototype.activateProspection = function(gameState) -{ - this.routeProspection = true; - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); - gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); -}; - -/** - * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute - * If an index is given, it returns the best route with this index or the best land route if index is a land index - */ -m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) -{ - let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); - let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); - if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon - { - this.tradeRoute = undefined; - this.potentialTradeRoute = undefined; - return false; - } - - let onlyOurs = !market2.hasEntities(); - if (onlyOurs) - market2 = market1; - let candidate = { "gain": 0 }; - let potential = { "gain": 0 }; - let bestIndex = { "gain": 0 }; - let bestLand = { "gain": 0 }; - - let mapSize = gameState.sharedScript.mapSize; - let traderTemplatesGains = gameState.getTraderTemplatesGains(); - - for (let m1 of market1.values()) - { - if (!m1.position()) - continue; - let access1 = m.getLandAccess(gameState, m1); - let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined; - for (let m2 of market2.values()) - { - if (onlyOurs && m1.id() >= m2.id()) - continue; - if (!m2.position()) - continue; - let access2 = m.getLandAccess(gameState, m2); - let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined; - let land = access1 == access2 ? access1 : undefined; - let sea = sea1 && sea1 == sea2 ? sea1 : undefined; - if (!land && !sea) - continue; - if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) - continue; - let gainMultiplier; - if (land && traderTemplatesGains.landGainMultiplier) - gainMultiplier = traderTemplatesGains.landGainMultiplier; - else if (sea && traderTemplatesGains.navalGainMultiplier) - gainMultiplier = traderTemplatesGains.navalGainMultiplier; - else - continue; - let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize)); - if (gain < this.minimalGain) - continue; - if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) - { - if (accessIndex) - { - if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) - { - if (gain < bestIndex.gain) - continue; - bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - else if (gameState.ai.accessibility.regionType[accessIndex] == "land") - { - if (gain < bestLand.gain) - continue; - bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - if (gain < candidate.gain) - continue; - candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - if (gain < potential.gain) - continue; - potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; - } - } - - if (potential.gain < 1) - this.potentialTradeRoute = undefined; - else - this.potentialTradeRoute = potential; - - if (candidate.gain < 1) - { - if (this.Config.debug > 2) - API3.warn("no better trade route possible"); - this.tradeRoute = undefined; - return false; - } - - if (this.Config.debug > 1 && this.tradeRoute) - { - if (candidate.gain > this.tradeRoute.gain) - API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain); - } - else if (this.Config.debug > 1) - API3.warn("one trade route set with gain " + candidate.gain); - this.tradeRoute = candidate; - - if (this.Config.chat) - { - let owner = this.tradeRoute.source.owner(); - if (owner == PlayerID) - owner = this.tradeRoute.target.owner(); - if (owner != PlayerID && !this.warnedAllies[owner]) - { // Warn an ally that we have a trade route with him - m.chatNewTradeRoute(gameState, owner); - this.warnedAllies[owner] = true; - } - } - - if (accessIndex) - { - if (bestIndex.gain > 0) - return bestIndex; - else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) - return bestLand; - return false; - } - return true; -}; - -/** Called when a market was built or destroyed, and checks if trader orders should be changed */ -m.TradeManager.prototype.checkTrader = function(gameState, ent) -{ - let presentRoute = ent.getMetadata(PlayerID, "route"); - if (!presentRoute) - return; - - if (!ent.position()) - { - // This trader is garrisoned, we will decide later (when ungarrisoning) what to do - ent.setMetadata(PlayerID, "route", undefined); - return; - } - - let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); - let possibleRoute = this.checkRoutes(gameState, access); - // Warning: presentRoute is from metadata, so contains entity ids - if (!possibleRoute || - possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target || - possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target) - { - // Trader will be assigned in updateTrader - ent.setMetadata(PlayerID, "route", undefined); - if (!possibleRoute && !ent.hasClass("Ship")) - { - let closestBase = m.getBestBase(gameState, ent, true); - if (closestBase.accessIndex == access) - { - let closestBasePos = closestBase.anchor.position(); - ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); - return; - } - } - ent.stopMoving(); - } -}; - -m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) -{ - if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market")) - return; - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) - return; - if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() && - !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities()) - return; - let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market")); - if (!template) - return; - this.checkRoutes(gameState); - let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); - if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain - { // no position found - if (gameState.getOwnEntitiesByClass("BarterMarket", 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) - API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + - marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); - else - API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + - marketPos[3]); - } - - if (!this.tradeRoute) - gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding); - let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); - if (!this.tradeRoute) - plan.queueToReset = "economicBuilding"; - queues.economicBuilding.addPlan(plan); -}; - -m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) -{ - if (expectedGain < this.minimalGain) - return false; - if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && - expectedGain < this.potentialTradeRoute.gain + 20) - return false; - return true; -}; - -m.TradeManager.prototype.update = function(gameState, events, queues) -{ - if (gameState.ai.HQ.canBarter) - this.performBarter(gameState); - - if (this.Config.difficulty <= 1) - return; - - if (this.checkEvents(gameState, events)) // true if one market was built or destroyed - { - this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); - this.checkRoutes(gameState); - } - - if (this.tradeRoute) - { - this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); - if (gameState.ai.playedTurn % 5 == 0) - this.trainMoreTraders(gameState, queues); - if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) - gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); - if (gameState.ai.playedTurn % 60 == 0) - this.setTradingGoods(gameState); - } - - if (this.routeProspection) - this.prospectForNewMarket(gameState, queues); -}; - -m.TradeManager.prototype.routeEntToId = function(route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - if (!route[key]) - return undefined; - ret[key] = route[key].id(); - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.routeIdToEnt = function(gameState, route) -{ - if (!route) - return undefined; - - let ret = {}; - for (let key in route) - { - if (key == "source" || key == "target") - { - ret[key] = gameState.getEntityById(route[key]); - if (!ret[key]) - return undefined; - } - else - ret[key] = route[key]; - } - return ret; -}; - -m.TradeManager.prototype.Serialize = function() -{ - return { - "tradeRoute": this.routeEntToId(this.tradeRoute), - "potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute), - "routeProspection": this.routeProspection, - "targetNumTraders": this.targetNumTraders, - "warnedAllies": this.warnedAllies - }; -}; - -m.TradeManager.prototype.Deserialize = function(gameState, data) -{ - for (let key in data) - { - if (key == "tradeRoute" || key == "potentialTradeRoute") - this[key] = this.routeIdToEnt(gameState, data[key]); - else - this[key] = data[key]; - } -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/transportPlan.js b/install/petraBased/petra-unitary/transportPlan.js deleted file mode 100644 index ee33a15..0000000 --- a/install/petraBased/petra-unitary/transportPlan.js +++ /dev/null @@ -1,729 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Describes a transport plan - * Constructor assign units (units is an ID array), a destination (position). - * The naval manager will try to deal with it accordingly. - * - * By this I mean that the naval manager will find how to go from access point 1 to access point 2 - * and then carry units from there. - * - * Note: only assign it units currently over land, or it won't work. - * Also: destination should probably be land, otherwise the units will be lost at sea. - * - * metadata for units: - * transport = this.ID - * onBoard = ship.id() when affected to a ship but not yet garrisoned - * = "onBoard" when garrisoned in a ship - * = undefined otherwise - * endPos = position of destination - * - * metadata for ships - * transporter = this.ID - */ - -m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) -{ - this.ID = gameState.ai.uniqueIDs.transports++; - this.debug = gameState.ai.Config.debug; - this.flotilla = false; // when false, only one ship per transport ... not yet tested when true - - this.endPos = endPos; - this.endIndex = endIndex; - this.startIndex = startIndex; - // TODO only cases with land-sea-land are allowed for the moment - // we could also have land-sea-land-sea-land - if (startIndex == 1) - { - // special transport from already garrisoned ship - if (!ship) - { - this.failed = true; - return false; - } - this.sea = ship.getMetadata(PlayerID, "sea"); - ship.setMetadata(PlayerID, "transporter", this.ID); - ship.setStance("none"); - for (let 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) - API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); - return false; - } - } - - for (let ent of units) - { - ent.setMetadata(PlayerID, "transport", this.ID); - ent.setMetadata(PlayerID, "endPos", endPos); - } - - if (this.debug > 1) - API3.warn("Starting a new transport plan with ID " + this.ID + - " to index " + endIndex + " with units length " + units.length); - - this.state = "boarding"; - this.boardingPos = {}; - this.needTransportShips = ship === undefined; - this.nTry = {}; - return true; -}; - -m.TransportPlan.prototype.init = function(gameState) -{ - this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "transport", this.ID)); - this.ships = gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); - - this.units.registerUpdates(); - this.ships.registerUpdates(); - this.transportShips.registerUpdates(); - - this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range -}; - -/** count available slots */ -m.TransportPlan.prototype.countFreeSlots = function() -{ - let slots = 0; - for (let ship of this.transportShips.values()) - slots += this.countFreeSlotsOnShip(ship); - return slots; -}; - -m.TransportPlan.prototype.countFreeSlotsOnShip = function(ship) -{ - if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) - return 0; - let occupied = ship.garrisoned().length + - this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", ship.id())).length; - return Math.max(ship.garrisonMax() - occupied, 0); -}; - -m.TransportPlan.prototype.assignUnitToShip = function(gameState, ent) -{ - if (this.needTransportShips) - return; - - for (let ship of this.transportShips.values()) - { - if (this.countFreeSlotsOnShip(ship) == 0) - continue; - ent.setMetadata(PlayerID, "onBoard", ship.id()); - if (this.debug > 1) - { - if (ent.getMetadata(PlayerID, "role") == "attack") - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); - else - Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); - } - return; - } - - if (this.flotilla) - { - this.needTransportShips = true; - return; - } - - if (!this.needSplit) - this.needSplit = [ent]; - else - this.needSplit.push(ent); -}; - -m.TransportPlan.prototype.assignShip = function(gameState) -{ - let pos; - // choose a unit of this plan not yet assigned to a ship - for (let ent of this.units.values()) - { - if (!ent.position() || ent.getMetadata(PlayerID, "onBoard") !== undefined) - continue; - pos = ent.position(); - break; - } - // and choose the nearest available ship from this unit - let distmin = Math.min(); - let nearest; - gameState.ai.HQ.navalManager.seaTransportShips[this.sea].forEach(ship => { - if (ship.getMetadata(PlayerID, "transporter")) - return; - if (pos) - { - let dist = API3.SquareVectorDistance(pos, ship.position()); - if (dist > distmin) - return; - distmin = dist; - nearest = ship; - } - else if (!nearest) - nearest = ship; - }); - if (!nearest) - return false; - - nearest.setMetadata(PlayerID, "transporter", this.ID); - nearest.setStance("none"); - this.ships.updateEnt(nearest); - this.transportShips.updateEnt(nearest); - this.needTransportShips = false; - return true; -}; - -/** add a unit to this plan */ -m.TransportPlan.prototype.addUnit = function(unit, endPos) -{ - unit.setMetadata(PlayerID, "transport", this.ID); - unit.setMetadata(PlayerID, "endPos", endPos); - this.units.updateEnt(unit); -}; - -/** remove a unit from this plan, if not yet on board */ -m.TransportPlan.prototype.removeUnit = function(gameState, unit) -{ - let 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); - let ship = gameState.getEntityById(shipId); - if (ship && !ship.garrisoned().length && - !this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", shipId)).length) - { - this.releaseShip(ship); - this.ships.updateEnt(ship); - this.transportShips.updateEnt(ship); - } - } -}; - -m.TransportPlan.prototype.releaseShip = function(ship) -{ - if (ship.getMetadata(PlayerID, "transporter") != this.ID) - { - API3.warn(" Petra: try removing a transporter ship with " + ship.getMetadata(PlayerID, "transporter") + - " from " + this.ID + " and stance " + ship.getStance()); - return; - } - - let defaultStance = ship.get("UnitAI/DefaultStance"); - if (defaultStance) - ship.setStance(defaultStance); - - ship.setMetadata(PlayerID, "transporter", undefined); - if (ship.getMetadata(PlayerID, "role") == "switchToTrader") - ship.setMetadata(PlayerID, "role", "trader"); -}; - -m.TransportPlan.prototype.releaseAll = function() -{ - for (let ship of this.ships.values()) - this.releaseShip(ship); - - for (let 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 */ -m.TransportPlan.prototype.cancelTransport = function(gameState) -{ - let ent = this.units.toEntityArray()[0]; - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchor || !base.anchor.position()) - { - for (let newbase of gameState.ai.HQ.baseManagers) - { - if (!newbase.anchor || !newbase.anchor.position()) - continue; - ent.setMetadata(PlayerID, "base", newbase.ID); - base = newbase; - break; - } - if (!base.anchor || !base.anchor.position()) - return false; - this.units.forEach(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. There are two states: - * - "boarding" means we're trying to board units onto our ships - * - "sailing" means we're moving ships and eventually unload units - * - then the plan is cleared - */ - -m.TransportPlan.prototype.update = function(gameState) -{ - if (this.state == "boarding") - this.onBoarding(gameState); - else if (this.state == "sailing") - this.onSailing(gameState); - - return this.units.length; -}; - -m.TransportPlan.prototype.onBoarding = function(gameState) -{ - let ready = true; - let time = gameState.ai.elapsedTime; - let shipTested = {}; - - for (let ent of this.units.values()) - { - if (!ent.getMetadata(PlayerID, "onBoard")) - { - ready = false; - this.assignUnitToShip(gameState, ent); - if (ent.getMetadata(PlayerID, "onBoard")) - { - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let 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; - let shipId = ent.getMetadata(PlayerID, "onBoard"); - let ship = gameState.getEntityById(shipId); - if (!ship) // the ship must have been destroyed - { - ent.setMetadata(PlayerID, "onBoard", undefined); - continue; - } - let distShip = API3.SquareVectorDistance(this.boardingPos[shipId], ship.position()); - if (!shipTested[shipId] && distShip > this.boardingRange) - { - shipTested[shipId] = true; - let retry = false; - let unitAIState = ship.unitAIState(); - if (unitAIState == "INDIVIDUAL.WALKING" || - unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") - { - if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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) - { - let oldPos = ent.getMetadata(PlayerID, "posGarrison"); - let 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) - API3.warn("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, 30); - ent.garrison(ship, true); - } - else if (API3.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 (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.state = "sailing"; - this.nTry = {}; - this.unloaded = []; - this.recovered = []; -}; - -/** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ -m.TransportPlan.prototype.isOnBoard = function(ent) -{ - for (let ship of this.transportShips.values()) - { - if (ship.garrisoned().indexOf(ent.id()) == -1) - continue; - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - return true; - } - return false; -}; - -/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ -m.TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, avoidEnnemy) -{ - if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex); - return destination; - } - else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - API3.warn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); - return destination; - } - - let startPos = ship.position(); - let distmin = Math.min(); - let posmin = destination; - let width = gameState.getPassabilityMap().width; - let cell = gameState.getPassabilityMap().cellSize; - let alliedDocks = gameState.getAllyStructures().filter(API3.Filters.and( - API3.Filters.byClass("Dock"), API3.Filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); - for (let i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) - { - let pos = [i%width+0.5, Math.floor(i/width)+0.5]; - pos = [cell*pos[0], cell*pos[1]]; - let dist = API3.VectorDistance(startPos, pos); - if (destination) - dist += API3.VectorDistance(pos, destination); - if (avoidEnnemy) - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) - dist += 100000000; - } - // require a small distance between all ships of the transport plan to avoid path finder problems - // this is also used when the ship is blocked and we want to find a new boarding point - for (let shipId in this.boardingPos) - if (this.boardingPos[shipId] !== undefined && - API3.SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) - dist += 1000000; - // and not too near our allied docks to not disturb naval traffic - let distSquare; - for (let dock of alliedDocks) - { - if (dock.foundationProgress() !== undefined) - distSquare = 900; - else - distSquare = 4900; - let dockDist = API3.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; -}; - -m.TransportPlan.prototype.onSailing = function(gameState) -{ - // Check that the units recovered on the previous turn have been reloaded - for (let recov of this.recovered) - { - let ent = gameState.getEntityById(recov.entId); - if (!ent) // entity destroyed - continue; - if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again - { - let ship = gameState.getEntityById(recov.shipId); - if (ship) - ship.moveApart(recov.entPos, 15); - continue; - } - if (this.debug > 1) - API3.warn(">>> transport " + this.ID + " reloading failed ... <<<"); - // destroy the unit if inaccessible otherwise leave it there - let index = m.getLandAccess(gameState, ent); - if (gameState.ai.HQ.landRegions[index]) - { - if (this.debug > 1) - API3.warn(" recovered entity kept " + ent.id()); - this.resetUnit(gameState, ent); - // TODO we should not destroy it, but now the unit could still be reloaded on the next turn - // and mess everything - ent.destroy(); - } - else - { - if (this.debug > 1) - API3.warn("recovered entity destroyed " + ent.id()); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - this.recovered = []; - - // Check that the units unloaded on the previous turn have been really unloaded and in the right position - let shipsToMove = {}; - for (let entId of this.unloaded) - { - let ent = gameState.getEntityById(entId); - if (!ent) // entity destroyed - continue; - else if (!ent.position()) // unloading failed - { - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship) - { - if (ship.garrisoned().indexOf(entId) != -1) - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - else - { - API3.warn("Petra transportPlan problem: unit not on ship without position ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - API3.warn("Petra transportPlan problem: unit on ship, but no ship ???"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else if (m.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) - API3.warn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); - let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); - if (ship && !this.canceled) - { - shipsToMove[ship.id()] = ship; - this.recovered.push({ "entId": ent.id(), "entPos": ent.position(), "shipId": ship.id() }); - ent.garrison(ship); - ent.setMetadata(PlayerID, "onBoard", "onBoard"); - } - else - { - if (this.debug > 1) - API3.warn("no way ... we destroy it"); - this.resetUnit(gameState, ent); - ent.destroy(); - } - } - else - { - // And make some room for other units - let pos = ent.position(); - let goal = ent.getMetadata(PlayerID, "endPos"); - let dist = goal ? API3.VectorDistance(pos, goal) : 0; - if (dist > 30) - ent.moveToRange(goal[0], goal[1], dist-20, dist-20); - else - ent.moveToRange(pos[0], pos[1], 20, 20); - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - } - } - for (let shipId in shipsToMove) - { - this.boardingPos[shipId] = this.getBoardingPos(gameState, shipsToMove[shipId], this.endIndex, this.sea, this.endPos, true); - shipsToMove[shipId].move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - this.unloaded = []; - - if (this.canceled) - { - for (let ship of this.ships.values()) - { - this.boardingPos[ship.id()] = undefined; - this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); - ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); - } - this.canceled = undefined; - } - - for (let ship of this.transportShips.values()) - { - if (ship.unitAIState() == "INDIVIDUAL.WALKING") - continue; - let shipId = ship.id(); - let dist = API3.SquareVectorDistance(ship.position(), this.boardingPos[shipId]); - let remaining = 0; - for (let entId of ship.garrisoned()) - { - let ent = gameState.getEntityById(entId); - if (!ent.getMetadata(PlayerID, "transport")) - continue; - remaining++; - if (dist < 625) - { - ship.unload(entId); - this.unloaded.push(entId); - ent.setMetadata(PlayerID, "onBoard", shipId); - } - } - - let recovering = 0; - for (let recov of this.recovered) - if (recov.shipId == shipId) - recovering++; - - if (!remaining && !recovering) // when empty, release the ship and move apart to leave room for other ships. TODO fight - { - ship.moveApart(this.boardingPos[shipId], 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) - API3.warn(shipId + " new attempt for a landing point "); - this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); - } - ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); - } - } -}; - -m.TransportPlan.prototype.resetUnit = function(gameState, ent) -{ - ent.setMetadata(PlayerID, "transport", undefined); - ent.setMetadata(PlayerID, "onBoard", undefined); - ent.setMetadata(PlayerID, "endPos", undefined); - // if from an army or attack, remove it - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); - if (attackPlan) - attackPlan.removeUnit(ent, true); - } - if (ent.getMetadata(PlayerID, "PartOfArmy")) - { - let army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); - if (army) - army.removeOwn(gameState, ent.id()); - } -}; - -m.TransportPlan.prototype.Serialize = function() -{ - return { - "ID": this.ID, - "flotilla": this.flotilla, - "endPos": this.endPos, - "endIndex": this.endIndex, - "startIndex": this.startIndex, - "sea": this.sea, - "state": this.state, - "boardingPos": this.boardingPos, - "needTransportShips": this.needTransportShips, - "nTry": this.nTry, - "canceled": this.canceled, - "unloaded": this.unloaded, - "recovered": this.recovered - }; -}; - -m.TransportPlan.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; - - this.failed = false; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/victoryManager.js b/install/petraBased/petra-unitary/victoryManager.js deleted file mode 100644 index e4a0453..0000000 --- a/install/petraBased/petra-unitary/victoryManager.js +++ /dev/null @@ -1,748 +0,0 @@ -var PETRA = function(m) -{ - -/** - * Handle events that are important to specific victory conditions: - * in capture_the_relic, capture gaia relics and train military guards. - * in regicide, train healer and military guards for the hero. - * in wonder, train military guards. - */ - -m.VictoryManager = function(Config) -{ - 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. - */ -m.VictoryManager.prototype.init = function(gameState) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let wonder of gameState.getOwnEntitiesByClass("Wonder", true).values()) - this.criticalEnts.set(wonder.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let hero of gameState.getOwnEntitiesByClass("Hero", true).values()) - { - let defaultStance = hero.hasClass("Soldier") ? "aggressive" : "passive"; - if (hero.getStance() != defaultStance) - hero.setStance(defaultStance); - this.criticalEnts.set(hero.id(), { - "garrisonEmergency": false, - "healersAssigned": 0, - "guardsAssigned": 0, // for non-healer guards - "guards": new Map() // ids of ents who are currently guarding this hero - }); - } - } - - if (gameState.getVictoryConditions().has("capture_the_relic")) - { - for (let relic of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).values()) - { - if (relic.owner() == PlayerID) - this.criticalEnts.set(relic.id(), { "guardsAssigned": 0, "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 - */ -m.VictoryManager.prototype.checkEvents = function(gameState, events) -{ - if (gameState.getVictoryConditions().has("wonder")) - { - for (let evt of events.Create) - { - let 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. - let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); - let builders = gameState.ai.HQ.bulkPickWorkers(gameState, base, 10); - if (builders) - for (let worker of builders.values()) - { - worker.setMetadata(PlayerID, "base", base.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - } - } - - for (let evt of events.ConstructionFinished) - { - if (!evt || !evt.newentity) - continue; - - let ent = gameState.getEntityById(evt.newentity); - if (ent && ent.isOwn(PlayerID) && ent.hasClass("Wonder")) - this.criticalEnts.set(ent.id(), { "guardsAssigned": 0, "guards": new Map() }); - } - } - - if (gameState.getVictoryConditions().has("regicide")) - { - for (let evt of events.Attacked) - { - if (!this.criticalEnts.has(evt.target)) - continue; - - let target = gameState.getEntityById(evt.target); - if (!target || !target.position() || target.healthLevel() > this.Config.garrisonHealthLevel.high) - continue; - - let plan = target.getMetadata(PlayerID, "plan"); - let hero = this.criticalEnts.get(evt.target); - if (plan != -2 && plan != -3) - { - target.stopMoving(); - - if (plan >= 0) - { - let attackPlan = gameState.ai.HQ.attackManager.getPlan(plan); - if (attackPlan) - attackPlan.removeUnit(target, true); - } - - if (target.getMetadata(PlayerID, "PartOfArmy")) - { - let 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 (let evt of events.TrainingFinished) - for (let entId of evt.entities) - { - let ent = gameState.getEntityById(entId); - if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "role") == "criticalEntHealer") - this.assignGuardToCriticalEnt(gameState, ent); - } - - for (let evt of events.Garrison) - { - if (!this.criticalEnts.has(evt.entity)) - continue; - - let hero = this.criticalEnts.get(evt.entity); - if (hero.garrisonEmergency) - hero.garrisonEmergency = false; - - let holderEnt = gameState.getEntityById(evt.holder); - if (!holderEnt) - continue; - - if (holderEnt.hasClass("Ship")) - { - // If the hero is garrisoned on a ship, remove its guards - for (let guardId of hero.guards.keys()) - { - let 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 (let guardId of hero.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt) - continue; - - let 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; - - let pos = holderEnt.position(); - let radius = holderEnt.obstructionRadius().max; - if (pos) - guardEnt.moveToRange(pos[0], pos[1], radius, radius + 5); - } - } - } - - for (let evt of events.EntityRenamed) - { - if (!this.guardEnts.has(evt.entity)) - continue; - for (let 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 (let evt of events.Destroy) - { - if (!evt.entityObj || evt.entityObj.owner() != PlayerID) - continue; - - let entId = evt.entityObj.id(); - if (this.criticalEnts.has(entId)) - { - this.removeCriticalEnt(gameState, entId); - continue; - } - - if (!this.guardEnts.has(entId)) - continue; - - for (let data of this.criticalEnts.values()) - if (data.guards.has(entId)) - { - data.guards.delete(entId); - if (evt.entityObj.hasClass("Healer")) - --data.healersAssigned; - else - --data.guardsAssigned; - break; - } - - this.guardEnts.delete(entId); - } - - for (let evt of events.UnGarrison) - { - if (!this.guardEnts.has(evt.entity) && !this.criticalEnts.has(evt.entity)) - continue; - - let 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") == "criticalEntHealer" || - ent.getMetadata(PlayerID, "role") == "criticalEntGuard") && !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 - let criticalEnt = this.criticalEnts.get(evt.entity); - for (let [id, isGuarding] of this.guardEnts) - { - if (criticalEnt.guards.size >= this.healersPerCriticalEnt) - break; - - if (!isGuarding) - { - let guardEnt = gameState.getEntityById(id); - if (guardEnt) - this.assignGuardToCriticalEnt(gameState, guardEnt, evt.entity); - } - } - } - - for (let 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; - - let 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": 0, "guards": new Map() }); - // Move captured relics to the closest base - if (ent.hasClass("Relic")) - this.pickCriticalEntRetreatLocation(gameState, ent, false); - } - } -}; - -m.VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId) -{ - for (let [guardId, role] of this.criticalEnts.get(criticalEntId).guards) - { - let 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 - */ -m.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 (let data of this.criticalEnts.values()) - { - if (data.healersAssigned === undefined || data.healersAssigned >= this.healersPerCriticalEnt) - continue; - let template = gameState.applyCiv("units/{civ}_support_healer_b"); - queues.healer.addPlan(new m.TrainingPlan(gameState, template, { "role": "criticalEntHealer", "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. - */ -m.VictoryManager.prototype.manageCriticalEntGuards = function(gameState) -{ - let numWorkers = gameState.getOwnEntitiesByRole("worker", true).length; - if (numWorkers < 20) - { - for (let data of this.criticalEnts.values()) - { - for (let guardId of data.guards.keys()) - { - let guardEnt = gameState.getEntityById(guardId); - if (!guardEnt || !guardEnt.hasClass("CitizenSoldier") || - guardEnt.getMetadata(PlayerID, "role") != "criticalEntGuard") - continue; - - guardEnt.removeGuard(); - guardEnt.setMetadata(PlayerID, "plan", -1); - guardEnt.setMetadata(PlayerID, "role", undefined); - this.guardEnts.delete(guardId); - --data.guardsAssigned; - - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - - if (++numWorkers >= 20) - break; - } - if (numWorkers >= 20) - break; - } - } - - let minWorkers = 25; - let deltaWorkers = 3; - for (let [id, data] of this.criticalEnts) - { - let criticalEnt = gameState.getEntityById(id); - if (!criticalEnt) - continue; - - let militaryGuardsPerCriticalEnt = (criticalEnt.hasClass("Wonder") ? 10 : 4) + - Math.round(this.Config.personality.defensive * 5); - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt) - continue; - - // First try to pick guards in the criticalEnt's accessIndex, to avoid unnecessary transports - for (let checkForSameAccess of [true, false]) - { - // First try to assign any Champion units we might have - for (let entity of gameState.getOwnEntitiesByClass("Champion", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.ai.HQ.attackManager.outOfPlan.values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - - for (let entity of gameState.getOwnEntitiesByClass("Soldier", true).values()) - { - if (!this.tryAssignMilitaryGuard(gameState, entity, criticalEnt, checkForSameAccess)) - continue; - --numWorkers; - if (++data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - - if (data.guardsAssigned >= militaryGuardsPerCriticalEnt || numWorkers <= minWorkers + deltaWorkers * data.guardsAssigned) - break; - } - } -}; - -m.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() || - m.getLandAccess(gameState, criticalEnt) != m.getLandAccess(gameState, guardEnt))) - return false; - - if (!this.assignGuardToCriticalEnt(gameState, guardEnt, criticalEnt.id())) - return false; - - guardEnt.setMetadata(PlayerID, "plan", -2); - guardEnt.setMetadata(PlayerID, "role", "criticalEntGuard"); - return true; -}; - -m.VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency) -{ - gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency); - let 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"); - let accessIndex = m.getLandAccess(gameState, criticalEnt); - let bestBase = m.getBestBase(gameState, criticalEnt, true); - if (bestBase.accessIndex == accessIndex) - { - let bestBasePos = bestBase.anchor.position(); - criticalEnt.move(bestBasePos[0], bestBasePos[1]); - } -}; - -/** - * 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). - */ -m.VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId) -{ - if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard()) - return false; - - if (criticalEntId && !this.criticalEnts.has(criticalEntId)) - { - criticalEntId = undefined; - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - } - - if (!criticalEntId) - { - let isHealer = guardEnt.hasClass("Healer"); - - // Assign to the critical ent with the fewest guards - let min = Math.min(); - for (let [id, data] of this.criticalEnts) - { - if (isHealer && (data.healersAssigned === undefined || data.healersAssigned > min)) - continue; - if (!isHealer && data.guardsAssigned > min) - continue; - - criticalEntId = id; - min = isHealer ? data.healersAssigned : data.guardsAssigned; - } - if (criticalEntId) - { - let data = this.criticalEnts.get(criticalEntId); - if (isHealer) - ++data.healersAssigned; - else - ++data.guardsAssigned; - } - } - - if (!criticalEntId) - { - if (guardEnt.getMetadata(PlayerID, "guardedEnt")) - guardEnt.setMetadata(PlayerID, "guardedEnt", undefined); - return false; - } - - let 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); - - let guardEntAccess = m.getLandAccess(gameState, guardEnt); - let criticalEntAccess = m.getLandAccess(gameState, criticalEnt); - if (guardEntAccess == criticalEntAccess) - { - let queued = m.returnResources(gameState, guardEnt); - guardEnt.guard(criticalEnt, queued); - let guardRole = guardEnt.getMetadata(PlayerID, "role") == "criticalEntHealer" ? "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; -}; - -m.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; -}; - -m.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 (let id of this.criticalEnts.keys()) - { - let 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 (let relicId of this.targetedGaiaRelics.keys()) - { - let 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 - let allGaiaRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == 0); - for (let relic of allGaiaRelics.values()) - { - let relicPosition = relic.position(); - if (!relicPosition || this.targetedGaiaRelics.has(relic.id())) - continue; - let 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 (let ent of gameState.getOwnUnits().values()) - { - if (!ent.position() || !ent.visionRange()) - continue; - if (API3.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. - */ -m.VictoryManager.prototype.captureGaiaRelic = function(gameState, relic) -{ - let capture = -relic.defaultRegenRate(); - let sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b); - let plans = this.targetedGaiaRelics.get(relic.id()); - for (let plan of plans) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (!attack) - continue; - for (let ent of attack.unitCollection.values()) - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - } - // No need to make a new attack if already enough units - if (capture > sumCapturePoints / 50) - return; - let relicPosition = relic.position(); - let access = m.getLandAccess(gameState, relic); - let 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; - let plan = ent.getMetadata(PlayerID, "plan"); - if (plan == -2 || plan == -3) - return false; - if (plan !== undefined && plan >= 0) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack && (attack.state != "unexecuted" || attack.type == "Raid")) - return false; - } - if (m.getLandAccess(gameState, ent) != access) - return false; - return true; - }).filterNearest(relicPosition); - let expedition = []; - for (let ent of units.values()) - { - capture += ent.captureStrength() * m.getAttackBonus(ent, relic, "Capture"); - expedition.push(ent); - if (capture > sumCapturePoints / 25) - break; - } - if (!expedition.length || !plans.length && capture < sumCapturePoints / 100) - return; - let attack = gameState.ai.HQ.attackManager.raidTargetEntity(gameState, relic); - if (!attack) - return; - let plan = attack.name; - attack.rallyPoint = undefined; - for (let 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); -}; - -m.VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId) -{ - for (let plan of this.targetedGaiaRelics.get(relicId)) - { - let attack = gameState.ai.HQ.attackManager.getPlan(plan); - if (attack) - attack.Abort(gameState); - } - this.targetedGaiaRelics.delete(relicId); -}; - -m.VictoryManager.prototype.Serialize = function() -{ - return { - "criticalEnts": this.criticalEnts, - "guardEnts": this.guardEnts, - "healersPerCriticalEnt": this.healersPerCriticalEnt, - "tryCaptureGaiaRelic": this.tryCaptureGaiaRelic, - "tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime, - "targetedGaiaRelics": this.targetedGaiaRelics - }; -}; - -m.VictoryManager.prototype.Deserialize = function(data) -{ - for (let key in data) - this[key] = data[key]; -}; - -return m; -}(PETRA); diff --git a/install/petraBased/petra-unitary/worker.js b/install/petraBased/petra-unitary/worker.js deleted file mode 100644 index bb4e2b5..0000000 --- a/install/petraBased/petra-unitary/worker.js +++ /dev/null @@ -1,1114 +0,0 @@ -var PETRA = function(m) -{ - -/** - * This class makes a worker do as instructed by the economy manager - */ - -m.Worker = function(base) -{ - this.ent = undefined; - this.base = base; - this.baseID = base.ID; -}; - -m.Worker.prototype.update = function(gameState, ent) -{ - if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) - return; - - let subrole = ent.getMetadata(PlayerID, "subrole"); - - // If we are waiting for a transport or we are sailing, just wait - if (ent.getMetadata(PlayerID, "transport") !== undefined) - { - // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board - if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined) - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target && plan && plan.state == "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 == "gatherer" || subrole == "hunter") - { - let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); - if (plan.state == "boarding" && ent.position()) - { - let hasDropsite = false; - let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; - for (let structure of gameState.getOwnStructures().values()) - { - if (m.getLandAccess(gameState, structure) != plan.endIndex) - continue; - let resourceDropsiteTypes = m.getBuiltEntity(gameState, structure).resourceDropsiteTypes(); - if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) - continue; - hasDropsite = true; - break; - } - if (!hasDropsite) - { - for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values()) - { - if (!unit.position() || m.getLandAccess(gameState, unit) != plan.endIndex) - continue; - let resourceDropsiteTypes = unit.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 = m.getLandAccess(gameState, ent); - // base 0 for unassigned entities has no accessIndex, so take the one from the entity - if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) - this.baseAccess = this.entAccess; - else - this.baseAccess = this.base.accessIndex; - - if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... - { - ent.setMetadata(PlayerID, "subrole", "idle"); - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - - this.ent = ent; - - let unitAIState = ent.unitAIState(); - if ((subrole == "hunter" || subrole == "gatherer") && - (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || - unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) - { - if (this.isInaccessibleSupply(gameState)) - { - if (this.retryWorking(gameState, subrole)) - return; - ent.stopMoving(); - } - - if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - // Check that we have not drifted too far when hunting - let target = gameState.getEntityById(orderData.target); - if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") - { - let 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)) - { - let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000; - let targetAccess = m.getLandAccess(gameState, target); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - let hasFoodDropsiteWithinDistance = false; - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (targetAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.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); - } - - let 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 == "fisher") - this.startFishing(gameState); - else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && - !ent.getMetadata(PlayerID, "PartOfArmy")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target) - { - let target = gameState.getEntityById(orderData.target); - if (target && (!target.position() || m.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")) - { - let orderData = ent.unitAIOrderData()[0]; - if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") - { - // If we are here, an enemy structure must have targeted one of our workers - // and UnitAI sent it fight back with allowCapture=true - let target = gameState.getEntityById(orderData.target); - if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) - ent.attack(orderData.target, m.allowCapture(gameState, ent, target)); - } - } - return; - } - - // Okay so we have a few tasks. - // If we're gathering, we'll check that we haven't run idle. - // And we'll also check that we're gathering a resource we want to gather. - - if (subrole == "gatherer") - { - if (ent.isIdle()) - { - // if we aren't storing resources or it's the same type as what we're about to gather, - // let's just pick a new resource. - // TODO if we already carry the max we can -> returnresources - if (!ent.resourceCarrying() || !ent.resourceCarrying().length || - ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) - { - this.startGathering(gameState); - } - else if (!m.returnResources(gameState, ent)) // try to deposit resources - { - // no dropsite, abandon old resources and start gathering new ones - this.startGathering(gameState); - } - } - else if (unitAIStateOrder == "GATHER") - { - // we're already gathering. But let's check if there is nothing better - // in case UnitAI did something bad - if (ent.unitAIOrderData().length) - { - let supplyId = ent.unitAIOrderData()[0].target; - let supply = gameState.getEntityById(supplyId); - if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") && - supply.resourceSupplyType().generic != "treasure" && - supplyId != ent.getMetadata(PlayerID, "supply")) - { - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); - if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (nearby.some(sup => sup.id == supplyId)) - ent.setMetadata(PlayerID, "supply", supplyId); - else if (nearby.length) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (medium.length && !medium.some(sup => sup.id == supplyId)) - { - gameState.ai.HQ.RemoveTCGatherer(supplyId); - this.startGathering(gameState); - } - else - ent.setMetadata(PlayerID, "supply", supplyId); - } - } - } - } - } - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - if (gameState.ai.playedTurn % 10 == 0) - { - // Check from time to time that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, this.ent); - } - - // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. - // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be - // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). - let gatherType = ent.getMetadata(PlayerID, "gather-type"); - let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; - if (influenceGroup && influenceGroup == "sparse") - { - let supplyId = ent.getMetadata(PlayerID, "supply"); - if (supplyId) - { - let nearby = this.base.dropsiteSupplies[gatherType].nearby; - if (!nearby.some(sup => sup.id == supplyId)) - { - if (nearby.length) - ent.setMetadata(PlayerID, "supply", undefined); - else - { - let medium = this.base.dropsiteSupplies[gatherType].medium; - if (!medium.some(sup => sup.id == supplyId) && medium.length) - ent.setMetadata(PlayerID, "supply", undefined); - } - } - } - } - } - } - else if (subrole == "builder") - { - if (unitAIStateOrder == "REPAIR") - { - // Update our target in case UnitAI sent us to a different foundation because of autocontinue - // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) - if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && - ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) - { - let targetId = ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (target && !target.hasClass("Field")) - { - ent.setMetadata(PlayerID, "target-foundation", targetId); - return; - } - ent.setMetadata(PlayerID, "target-foundation", undefined); - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) - let targetId = ent.getMetadata(PlayerID, "target-foundation"); - if (targetId && gameState.getEntityById(targetId)) - return; - ent.stopMoving(); - } - // okay so apparently we aren't working. - // Unless we've been explicitely told to keep our role, make us idle. - let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) - { - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.setMetadata(PlayerID, "target-foundation", undefined); - // If worker elephant, move away to avoid being trapped in between constructions - if (ent.hasClass("Elephant")) - this.moveToGatherer(gameState, ent, true); - else if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) - { - // reassign it to something useful - this.base.reassignIdleWorkers(gameState, [ent]); - this.update(gameState, ent); - return; - } - } - else - { - let goalAccess = m.getLandAccess(gameState, target); - let queued = m.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 == "hunter") - { - let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); - if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) - { - if (!this.startHunting(gameState)) - { - // nothing to hunt around. Try another region if any - let nowhereToHunt = true; - for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - let basePos = base.anchor.position(); - if (this.startHunting(gameState, basePos)) - { - ent.setMetadata(PlayerID, "base", base.ID); - 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" || unitAIStateOrder == "RETURNRESOURCE") - { - // we may have drifted towards ennemy territory during the hunt, if yes go home - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startHunting(gameState); - else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") - { - // Check that UnitAI does not send us to an inaccessible dropsite - let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); - if (dropsite && dropsite.position() && this.entAccess != m.getLandAccess(gameState, dropsite)) - m.returnResources(gameState, ent); - } - } - } - } - else if (subrole == "fisher") - { - if (ent.isIdle()) - this.startFishing(gameState); - else // if we have drifted towards ennemy territory during the fishing, go home - { - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - this.startFishing(gameState); - } - } -}; - -m.Worker.prototype.retryWorking = function(gameState, subrole) -{ - switch (subrole) - { - case "gatherer": - return this.startGathering(gameState); - case "hunter": - return this.startHunting(gameState); - case "fisher": - return this.startFishing(gameState); - case "builder": - return this.startBuilding(gameState); - default: - return false; - } -}; - -m.Worker.prototype.startBuilding = function(gameState) -{ - let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); - if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) - return false; - if (m.getLandAccess(gameState, target) != this.entAccess) - return false; - this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses - return true; -}; - -m.Worker.prototype.startGathering = function(gameState) -{ - // First look for possible treasure if any - if (m.gatherTreasure(gameState, this.ent)) - return true; - - let resource = this.ent.getMetadata(PlayerID, "gather-type"); - - // If we are gathering food, try to hunt first - if (resource == "food" && this.startHunting(gameState)) - return true; - - let findSupply = function(ent, supplies) { - let ret = false; - let gatherRates = ent.resourceGatherRates(); - for (let i = 0; i < supplies.length; ++i) - { - // exhausted resource, remove it from this list - if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) - { - supplies.splice(i--, 1); - continue; - } - if (m.IsSupplyFull(gameState, supplies[i].ent)) - continue; - let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - let supplyType = supplies[i].ent.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); - if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && - supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - // not in ennemy territory - let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); - if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally - continue; - gameState.ai.HQ.AddTCGatherer(supplies[i].id); - ent.setMetadata(PlayerID, "supply", supplies[i].id); - ret = supplies[i].ent; - break; - } - return ret; - }; - - let navalManager = gameState.ai.HQ.navalManager; - let supply; - - // first look in our own base if accessible from our present position - if (this.baseAccess == this.entAccess) - { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.gather(supply); - return true; - } - // --> for food, try to gather from fields if any, otherwise build one if any - if (resource == "food") - { - supply = this.gatherNearestField(gameState, this.baseID); - if (supply) - { - this.ent.gather(supply); - return true; - } - supply = this.buildAnyField(gameState, this.baseID); - if (supply) - { - this.ent.repair(supply); - return true; - } - } - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - // So if we're here we have checked our whole base for a proper resource (or it was not accessible) - // --> check other bases directly accessible - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any - { - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - - // Okay may-be we haven't found any appropriate dropsite anywhere. - // Try to help building one if any accessible foundation available - let foundations = gameState.getOwnFoundations().toEntityArray(); - let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { - if (!foundation || m.getLandAccess(gameState, foundation) != this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - if (foundation.getMetadata(PlayerID, "base") != this.baseID) - this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); - this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); - this.ent.setMetadata(PlayerID, "subrole", "builder"); - this.ent.repair(foundation); - return true; - } - return false; - }, this); - if (shouldBuild) - return true; - - // Still nothing ... try bases which need a transport - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 (let 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 (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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 || m.getLandAccess(gameState, foundation) == this.entAccess) - return false; - let structure = gameState.getBuiltTemplate(foundation.templateName()); - if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) - { - let foundationAccess = m.getLandAccess(gameState, foundation); - if (navalManager.requireTransport(gameState, this.ent, 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", "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.ent, this.base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.ID == this.baseID) - continue; - if (base.accessIndex != this.entAccess) - continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); - if (supply) - { - this.ent.setMetadata(PlayerID, "base", base.ID); - this.ent.gather(supply); - return true; - } - } - for (let base of gameState.ai.HQ.baseManagers) - { - if (base.accessIndex == this.entAccess) - continue; - supply = findSupply(this.ent, 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) - API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -/** - * if position is given, we only check if we could hunt from this position but do nothing - * otherwise the position of the entity is taken, and if something is found, we directly start the hunt - */ -m.Worker.prototype.startHunting = function(gameState, position) -{ - // First look for possible treasure if any - if (!position && m.gatherTreasure(gameState, this.ent)) - return true; - - let resources = gameState.getHuntableSupplies(); - if (!resources.hasEntities()) - return false; - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let isCavalry = this.ent.hasClass("Cavalry"); - let isRanged = this.ent.hasClass("Ranged"); - let entPosition = position ? position : this.ent.position(); - let foodDropsites = gameState.playerData.hasSharedDropsites ? - gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); - - let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) - { - for (let dropsite of foodDropsites.values()) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (supplyAccess != m.getLandAccess(gameState, dropsite)) - continue; - if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) - return true; - } - return false; - }; - - let gatherRates = this.ent.resourceGatherRates(); - for (let supply of resources.values()) - { - if (!supply.position()) - continue; - - let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - continue; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - if (m.IsSupplyFull(gameState, supply)) - continue; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - continue; - - let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; - // Only cavalry and range units should hunt fleeing animals - if (canFlee && !isCavalry && !isRanged) - continue; - - let supplyAccess = m.getLandAccess(gameState, supply); - if (supplyAccess != this.entAccess) - continue; - - // measure the distance to the resource - let dist = API3.SquareVectorDistance(entPosition, supply.position()); - if (dist > nearestSupplyDist) - continue; - - // Only cavalry should hunt faraway - if (!isCavalry && dist > 25000) - continue; - - // Avoid ennemy territory - let 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 cavalry should hunt far from dropsite (specially for non domestic animals which flee) - if (!isCavalry && canFlee && territoryOwner == 0) - continue; - let distanceSquare = isCavalry ? 35000 : (canFlee ? 7000 : 12000); - if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) - continue; - - nearestSupplyDist = dist; - nearestSupply = supply; - } - - if (nearestSupply) - { - if (position) - return true; - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - return false; -}; - -m.Worker.prototype.startFishing = function(gameState) -{ - if (!this.ent.position()) - return false; - - let resources = gameState.getFishableSupplies(); - if (!resources.hasEntities()) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState); - this.ent.destroy(); - return false; - } - - let nearestSupplyDist = Math.min(); - let nearestSupply; - - let fisherSea = m.getSeaAccess(gameState, this.ent); - let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")). - filter(API3.Filters.byClass("Dock")).toEntityArray(); - - let nearestDropsiteDist = function(supply) { - let distMin = 1000000; - let pos = supply.position(); - for (let dropsite of fishDropsites) - { - if (!dropsite.position()) - continue; - let owner = dropsite.owner(); - // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again - if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) - continue; - if (fisherSea != m.getSeaAccess(gameState, dropsite)) - continue; - distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); - } - return distMin; - }; - - let exhausted = true; - let gatherRates = this.ent.resourceGatherRates(); - resources.forEach(function(supply) - { - if (!supply.position()) - return; - - // check that it is accessible - if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) - return; - - exhausted = false; - - let supplyType = supply.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - return; - - if (m.IsSupplyFull(gameState, supply)) - return; - // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); - if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) - return; - - // Avoid ennemy territory - if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) - return; - - // measure the distance from the resource to the nearest dropsite - let dist = nearestDropsiteDist(supply); - if (dist > nearestSupplyDist) - return; - - nearestSupplyDist = dist; - nearestSupply = supply; - }); - - if (exhausted) - { - gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); - this.ent.destroy(); - return false; - } - - if (nearestSupply) - { - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); - this.ent.gather(nearestSupply); - this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); - this.ent.setMetadata(PlayerID, "target-foundation", undefined); - return true; - } - if (this.ent.getMetadata(PlayerID, "subrole") == "fisher") - this.ent.setMetadata(PlayerID, "subrole", "idle"); - return false; -}; - -m.Worker.prototype.gatherNearestField = function(gameState, baseID) -{ - let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); - let bestFarm; - - let gatherRates = this.ent.resourceGatherRates(); - for (let field of ownFields.values()) - { - if (m.IsSupplyFull(gameState, field)) - continue; - let supplyType = field.get("ResourceSupply/Type"); - if (!gatherRates[supplyType]) - continue; - - let rate = 1; - let diminishing = field.getDiminishingReturns(); - if (diminishing < 1) - { - let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); - if (num > 0) - rate = Math.pow(diminishing, num); - } - // Add a penalty distance depending on rate - let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; - if (!bestFarm || dist < bestFarm.dist) - bestFarm = { "ent": field, "dist": dist, "rate": rate }; - } - // If other field foundations available, better build them when rate becomes too small - if (!bestFarm || bestFarm.rate < 0.70 && - gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) - return false; - gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); - this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); - return bestFarm.ent; -}; - -/** - * WARNING with the present options of AI orders, the unit will not gather after building the farm. - * This is done by calling the gatherNearestField function when construction is completed. - */ -m.Worker.prototype.buildAnyField = function(gameState, baseID) -{ - if (!this.ent.isBuilder()) - return false; - let bestFarmEnt = false; - let bestFarmDist = 10000000; - let pos = this.ent.position(); - for (let found of gameState.getOwnFoundations().values()) - { - if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) - continue; - let current = found.getBuildersNb(); - if (current === undefined || - current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) - continue; - let dist = API3.SquareVectorDistance(found.position(), pos); - if (dist > bestFarmDist) - continue; - bestFarmEnt = found; - bestFarmDist = dist; - } - return bestFarmEnt; -}; - -/** - * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. - * For the time being, we move towards the nearest gatherer (providing him a dropsite). - * BaseManager does also use that function to deal with its mobile dropsites. - */ -m.Worker.prototype.moveToGatherer = function(gameState, ent, forced) -{ - let pos = ent.position(); - if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) - return; - if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) - return; - let gatherers = this.base.workersBySubrole(gameState, "gatherer"); - let dist = Math.min(); - let destination; - let access = m.getLandAccess(gameState, ent); - let types = ent.resourceDropsiteTypes(); - for (let gatherer of gatherers.values()) - { - let gathererType = gatherer.getMetadata(PlayerID, "gather-type"); - if (!gathererType || types.indexOf(gathererType) == -1) - continue; - if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || - m.getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) - continue; - let distance = API3.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). - */ -m.Worker.prototype.isInaccessibleSupply = function(gameState) -{ - if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) - return false; - let targetId = this.ent.unitAIOrderData()[0].target; - let target = gameState.getEntityById(targetId); - if (!target) - return true; - - if (!target.resourceSupplyType()) - return false; - - let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); - let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; - if (!approachingTarget || approachingTarget != targetId) - { - this.ent.setMetadata(PlayerID, "approachingTarget", targetId); - this.ent.setMetadata(PlayerID, "approachingTime", undefined); - this.ent.setMetadata(PlayerID, "approachingPos", undefined); - this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); - let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); - if (alreadyTried && alreadyTried != targetId) - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - } - - let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); - if (carriedBefore != carriedAmount) - { - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.setMetadata(PlayerID, "alreadyTried", undefined); - if (target.getMetadata(PlayerID, "inaccessibleTime")) - target.setMetadata(PlayerID, "inaccessibleTime", 0); - return false; - } - - let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); - if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) - return true; - - let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); - if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) - { - let presentPos = this.ent.position(); - let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); - if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) - { - this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); - this.ent.setMetadata(PlayerID, "approachingPos", presentPos); - return false; - } - if (gameState.ai.elapsedTime - approachingTime > 10) - { - if (this.ent.getMetadata(PlayerID, "alreadyTried")) - { - target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); - return true; - } - // let's try again to reach it - this.ent.setMetadata(PlayerID, "alreadyTried", targetId); - this.ent.setMetadata(PlayerID, "approachingTarget", undefined); - this.ent.gather(target); - return false; - } - } - return false; -}; - -return m; -}(PETRA); diff --git a/mod.json b/mod.json index 4650362..fdfa603 100644 --- a/mod.json +++ b/mod.json @@ -1,8 +1,8 @@ { "name": "ArchAIPack", - "version": "0.1.2", + "version": "0.1.3", "label": "Arch AI Pack", - "description": "Arch Bot is a modified version of original Petra Bot AI. This pack includes 9 Arch based AI bots (Admiral, Capitalist, Communist, Imperialist, Mason, Mercantilist, Patriot, Theocrat and Unitary) and 4 Petra based AI bots (Imperialist, Patriot, Single Base and Unitary)", + "description": "Arch Bot is a modified version of original Petra Bot AI. This pack includes 9 Arch based AI bots (Admiral, Capitalist, Communist, Imperialist, Mason, Mercantilist, Patriot, Theocrat and Unitary) and 4 Petra based AI bots (Imperialist, Patriot, Single Based and Unitary)", "dependencies": [ "0ad>=0.0.23" ] diff --git a/install/petraBased/petra-patriot/_petrabot.js b/simulation/ai/petra-dev/_petrabot.js similarity index 97% rename from install/petraBased/petra-patriot/_petrabot.js rename to simulation/ai/petra-dev/_petrabot.js index 3e5d861..82aedcc 100644 --- a/install/petraBased/petra-patriot/_petrabot.js +++ b/simulation/ai/petra-dev/_petrabot.js @@ -3,7 +3,15 @@ Engine.IncludeModule("common-api"); var PETRA = (function() { var m = {}; +/// Patriot m.maxBaseCount = 1; +/// Patriot +/// SingleBased +m.maxBaseCount = 1; +/// SingleBased +/// Unitary +m.maxBaseCount = 1; +/// Unitary m.PetraBot = function PetraBot(settings) { diff --git a/install/petraBased/petra-imperialist/attackManager.js b/simulation/ai/petra-dev/attackManager.js similarity index 100% rename from install/petraBased/petra-imperialist/attackManager.js rename to simulation/ai/petra-dev/attackManager.js diff --git a/install/petraBased/petra-patriot/attackPlan.js b/simulation/ai/petra-dev/attackPlan.js similarity index 99% rename from install/petraBased/petra-patriot/attackPlan.js rename to simulation/ai/petra-dev/attackPlan.js index 677fd55..f685642 100644 --- a/install/petraBased/petra-patriot/attackPlan.js +++ b/simulation/ai/petra-dev/attackPlan.js @@ -1214,7 +1214,6 @@ m.AttackPlan.prototype.setRallyPoint = function(gameState) */ m.AttackPlan.prototype.StartAttack = function(gameState) { - if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); @@ -1227,10 +1226,21 @@ m.AttackPlan.prototype.StartAttack = 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"); - +/// Imperialist // Wait to attack! if( gameState.getPopulation() < 0.85*gameState.getPopulationMax() ) return false; +/// Imperialist +/// Patriot + // Wait to attack! + if( gameState.getPopulation() < 0.85*gameState.getPopulationMax() ) + return false; +/// Patriot +/// Unitary + // Wait to attack! + if( gameState.getPopulation() < 0.85*gameState.getPopulationMax() ) + return false; +/// Unitary for (let ent of this.unitCollection.values()) { diff --git a/install/petraBased/petra-imperialist/baseManager.js b/simulation/ai/petra-dev/baseManager.js similarity index 100% rename from install/petraBased/petra-imperialist/baseManager.js rename to simulation/ai/petra-dev/baseManager.js diff --git a/install/petraBased/petra-imperialist/buildManager.js b/simulation/ai/petra-dev/buildManager.js similarity index 100% rename from install/petraBased/petra-imperialist/buildManager.js rename to simulation/ai/petra-dev/buildManager.js diff --git a/install/petraBased/petra-imperialist/chatHelper.js b/simulation/ai/petra-dev/chatHelper.js similarity index 100% rename from install/petraBased/petra-imperialist/chatHelper.js rename to simulation/ai/petra-dev/chatHelper.js diff --git a/install/petraBased/petra-imperialist/config.js b/simulation/ai/petra-dev/config.js similarity index 100% rename from install/petraBased/petra-imperialist/config.js rename to simulation/ai/petra-dev/config.js diff --git a/simulation/ai/petra-dev/data.json b/simulation/ai/petra-dev/data.json new file mode 100644 index 0000000..a0e23e4 --- /dev/null +++ b/simulation/ai/petra-dev/data.json @@ -0,0 +1,7 @@ +{ + "name": "Petra Bot", + "description": "Petra is the default 0 A.D. AI bot. Please report issues to Wildfire Games (see the link in the main menu).\n\nThe AI has a bonus/penalty on resource stockpiling (either gathering rate or trade gain) varying from -60% for Sandbox to +60% for Very Hard (Medium 0%) and the easiest levels have a slower production and building rate. In addition, the Sandbox level does not expand nor attack.", + "moduleName" : "PETRA", + "constructor": "PetraBot", + "useShared": true +} diff --git a/install/petraBased/petra-imperialist/defenseArmy.js b/simulation/ai/petra-dev/defenseArmy.js similarity index 100% rename from install/petraBased/petra-imperialist/defenseArmy.js rename to simulation/ai/petra-dev/defenseArmy.js diff --git a/install/petraBased/petra-imperialist/defenseManager.js b/simulation/ai/petra-dev/defenseManager.js similarity index 100% rename from install/petraBased/petra-imperialist/defenseManager.js rename to simulation/ai/petra-dev/defenseManager.js diff --git a/install/petraBased/petra-imperialist/diplomacyManager.js b/simulation/ai/petra-dev/diplomacyManager.js similarity index 100% rename from install/petraBased/petra-imperialist/diplomacyManager.js rename to simulation/ai/petra-dev/diplomacyManager.js diff --git a/install/petraBased/petra-imperialist/entityExtend.js b/simulation/ai/petra-dev/entityExtend.js similarity index 100% rename from install/petraBased/petra-imperialist/entityExtend.js rename to simulation/ai/petra-dev/entityExtend.js diff --git a/install/petraBased/petra-imperialist/garrisonManager.js b/simulation/ai/petra-dev/garrisonManager.js similarity index 100% rename from install/petraBased/petra-imperialist/garrisonManager.js rename to simulation/ai/petra-dev/garrisonManager.js diff --git a/install/petraBased/petra-single-base/headquarters.js b/simulation/ai/petra-dev/headquarters.js similarity index 99% rename from install/petraBased/petra-single-base/headquarters.js rename to simulation/ai/petra-dev/headquarters.js index 407e77b..e16d4c8 100644 --- a/install/petraBased/petra-single-base/headquarters.js +++ b/simulation/ai/petra-dev/headquarters.js @@ -1803,8 +1803,18 @@ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { +/// Patriot if (this.numPotentialBases() >= m.maxBaseCount) return false; +/// Patriot +/// SingleBased + if (this.numPotentialBases() >= m.maxBaseCount) + return false; +/// SingleBased +/// Unitary + if (this.numPotentialBases() >= m.maxBaseCount) + return false; +/// Unitary if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) diff --git a/install/petraBased/petra-imperialist/mapModule.js b/simulation/ai/petra-dev/mapModule.js similarity index 100% rename from install/petraBased/petra-imperialist/mapModule.js rename to simulation/ai/petra-dev/mapModule.js diff --git a/install/petraBased/petra-imperialist/navalManager.js b/simulation/ai/petra-dev/navalManager.js similarity index 100% rename from install/petraBased/petra-imperialist/navalManager.js rename to simulation/ai/petra-dev/navalManager.js diff --git a/install/petraBased/petra-imperialist/queue.js b/simulation/ai/petra-dev/queue.js similarity index 100% rename from install/petraBased/petra-imperialist/queue.js rename to simulation/ai/petra-dev/queue.js diff --git a/install/petraBased/petra-unitary/queueManager.js b/simulation/ai/petra-dev/queueManager.js similarity index 95% rename from install/petraBased/petra-unitary/queueManager.js rename to simulation/ai/petra-dev/queueManager.js index fc0c9dc..40949bd 100644 --- a/install/petraBased/petra-unitary/queueManager.js +++ b/simulation/ai/petra-dev/queueManager.js @@ -251,7 +251,24 @@ m.QueueManager.prototype.distributeResources = function(gameState) this.switchResource(gameState, res); continue; } - +/// Imperialist + for (let q in this.queues) { + let queueCost = this.queues[q].maxAccountWanted(gameState, 0); + if (this.queues[q].hasQueuedUnits() && this.accounts[q][res] < queueCost[res] && !this.queues[q].paused) { + this.accounts[q][res] = queueCost[res]; + } + } + continue; +/// Imperialist +/// Patriot + for (let q in this.queues) { + let queueCost = this.queues[q].maxAccountWanted(gameState, 0); + if (this.queues[q].hasQueuedUnits() && this.accounts[q][res] < queueCost[res] && !this.queues[q].paused) { + this.accounts[q][res] = queueCost[res]; + } + } + continue; +/// Patriot let totalPriority = 0; let tempPrio = {}; let maxNeed = {}; diff --git a/install/petraBased/petra-imperialist/queueplan.js b/simulation/ai/petra-dev/queueplan.js similarity index 100% rename from install/petraBased/petra-imperialist/queueplan.js rename to simulation/ai/petra-dev/queueplan.js diff --git a/install/petraBased/petra-imperialist/queueplanBuilding.js b/simulation/ai/petra-dev/queueplanBuilding.js similarity index 100% rename from install/petraBased/petra-imperialist/queueplanBuilding.js rename to simulation/ai/petra-dev/queueplanBuilding.js diff --git a/install/petraBased/petra-imperialist/queueplanResearch.js b/simulation/ai/petra-dev/queueplanResearch.js similarity index 100% rename from install/petraBased/petra-imperialist/queueplanResearch.js rename to simulation/ai/petra-dev/queueplanResearch.js diff --git a/install/petraBased/petra-imperialist/queueplanTraining.js b/simulation/ai/petra-dev/queueplanTraining.js similarity index 100% rename from install/petraBased/petra-imperialist/queueplanTraining.js rename to simulation/ai/petra-dev/queueplanTraining.js diff --git a/install/petraBased/petra-imperialist/researchManager.js b/simulation/ai/petra-dev/researchManager.js similarity index 100% rename from install/petraBased/petra-imperialist/researchManager.js rename to simulation/ai/petra-dev/researchManager.js diff --git a/install/petraBased/petra-imperialist/startingStrategy.js b/simulation/ai/petra-dev/startingStrategy.js similarity index 100% rename from install/petraBased/petra-imperialist/startingStrategy.js rename to simulation/ai/petra-dev/startingStrategy.js diff --git a/install/petraBased/petra-imperialist/tradeManager.js b/simulation/ai/petra-dev/tradeManager.js similarity index 100% rename from install/petraBased/petra-imperialist/tradeManager.js rename to simulation/ai/petra-dev/tradeManager.js diff --git a/install/petraBased/petra-imperialist/transportPlan.js b/simulation/ai/petra-dev/transportPlan.js similarity index 100% rename from install/petraBased/petra-imperialist/transportPlan.js rename to simulation/ai/petra-dev/transportPlan.js diff --git a/install/petraBased/petra-imperialist/victoryManager.js b/simulation/ai/petra-dev/victoryManager.js similarity index 100% rename from install/petraBased/petra-imperialist/victoryManager.js rename to simulation/ai/petra-dev/victoryManager.js diff --git a/install/petraBased/petra-imperialist/worker.js b/simulation/ai/petra-dev/worker.js similarity index 100% rename from install/petraBased/petra-imperialist/worker.js rename to simulation/ai/petra-dev/worker.js