diff --git a/binaries/data/mods/public/simulation/ai/aegis/_Read Me.txt b/binaries/data/mods/public/simulation/ai/aegis/_Read Me.txt index c5fb97b380..46d7530f96 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/_Read Me.txt +++ b/binaries/data/mods/public/simulation/ai/aegis/_Read Me.txt @@ -10,4 +10,4 @@ Please report any error to the wildfire games forum ( http://www.wildfiregames.c Requires common-api-v3. -(note: no saved game support as of yet). \ No newline at end of file +(note: no saved game support as of yet). diff --git a/binaries/data/mods/public/simulation/ai/aegis/aegis.js b/binaries/data/mods/public/simulation/ai/aegis/aegis.js index 0672019a67..e4d608ed35 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/aegis.js +++ b/binaries/data/mods/public/simulation/ai/aegis/aegis.js @@ -1,4 +1,10 @@ -function QBotAI(settings) { +// "local" global variables for stuffs that will need a unique ID +// Note that since order of loading is alphabetic, this means this file must go before any other file using them. +var uniqueIDBOPlans = 0; // training/building/research plans +var uniqueIDBases = 1; // base manager ID. Starts at one because "0" means "no base" on the map +var uniqueIDTPlans = 1; // transport plans. starts at 1 because 0 might be used as none. + +function AegisBot(settings) { BaseAI.call(this, settings); Config.updateDifficulty(settings.difficulty); @@ -6,47 +12,33 @@ function QBotAI(settings) { this.turn = 0; this.playedTurn = 0; - - this.modules = { - "economy": new EconomyManager(), - "military": new MilitaryAttackManager() - }; + + this.priorities = Config.priorities; // this.queues can only be modified by the queue manager or things will go awry. - this.queues = { - house : new Queue(), - citizenSoldier : new Queue(), - villager : new Queue(), - economicBuilding : new Queue(), - dropsites : new Queue(), - field : new Queue(), - militaryBuilding : new Queue(), - defenceBuilding : new Queue(), - civilCentre: new Queue(), - majorTech: new Queue(), - minorTech: new Queue() - }; - - this.productionQueues = []; - - this.priorities = Config.priorities; - + this.queues = {}; + for (i in this.priorities) + this.queues[i] = new Queue(); + this.queueManager = new QueueManager(this.queues, this.priorities); - + + this.HQ = new HQ(); + this.firstTime = true; this.savedEvents = []; - this.waterMap = false; - this.defcon = 5; this.defconChangeTime = -10000000; } -QBotAI.prototype = new BaseAI(); +AegisBot.prototype = new BaseAI(); + +AegisBot.prototype.InitShared = function(gameState, sharedScript) { + + this.HQ.init(gameState,sharedScript.events,this.queues); + debug ("Initialized with the difficulty " + Config.difficulty); -// Bit of a hack: I run the pathfinder early, before the map apears, to avoid a sometimes substantial lag right at the start. -QBotAI.prototype.InitShared = function(gameState, sharedScript) { var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID)); var myKeyEntities = ents.filter(function(ent) { return ent.hasClass("CivCentre"); @@ -63,6 +55,8 @@ QBotAI.prototype.InitShared = function(gameState, sharedScript) { enemyKeyEntities = gameState.getEntities().filter(Filters.not(Filters.byOwner(PlayerID))); } + this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position()); + this.pathFinder = new aStarPath(gameState, false, true); this.pathsToMe = []; this.pathInfo = { "angle" : 0, "needboat" : true, "mkeyPos" : myKeyEntities.toEntityArray()[0].position(), "ekeyPos" : enemyKeyEntities.toEntityArray()[0].position() }; @@ -82,92 +76,28 @@ QBotAI.prototype.InitShared = function(gameState, sharedScript) { } this.pathInfo.angle += Math.PI/3.0; -} - -//Some modules need the gameState to fully initialise -QBotAI.prototype.runInit = function(gameState, events){ this.chooseRandomStrategy(); +} - for (var i in this.modules){ - if (this.modules[i].init){ - this.modules[i].init(gameState, events); - } - } - debug ("Inited, diff is " + Config.difficulty); - this.timer = new Timer(); - - - var ents = gameState.getOwnEntities(); - var myKeyEntities = gameState.getOwnEntities().filter(function(ent) { - return ent.hasClass("CivCentre"); - }); - - if (myKeyEntities.length == 0){ - myKeyEntities = gameState.getOwnEntities(); - } - - // disband the walls themselves - if (gameState.playerData.civ == "iber") { - gameState.getOwnEntities().filter(function(ent) { //}){ - if (ent.hasClass("StoneWall") && !ent.hasClass("Tower")) - ent.destroy(); - }); - } - - var filter = Filters.byClass("CivCentre"); - var enemyKeyEntities = gameState.getEnemyEntities().filter(filter); - - if (enemyKeyEntities.length == 0){ - enemyKeyEntities = gameState.getEnemyEntities(); - } - - //this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position()); - - this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position()); - - if (enemyKeyEntities.length == 0) - return; - - this.templateManager = new TemplateManager(gameState); -}; - -QBotAI.prototype.OnUpdate = function(sharedScript) { +AegisBot.prototype.OnUpdate = function(sharedScript) { if (this.gameFinished){ return; } - if (this.events.length > 0){ + if (this.events.length > 0 && this.turn !== 0){ this.savedEvents = this.savedEvents.concat(this.events); } - // Run the update every n turns, offset depending on player ID to balance the load - // this also means that init at turn 0 always happen and is never run in parallel to the first played turn so I use an else if. - if (this.turn == 0) { + if ((this.turn + this.player) % 8 == 5) { - //Engine.DumpImage("terrain.png", this.accessibility.map, this.accessibility.width,this.accessibility.height,255) - //Engine.DumpImage("Access.png", this.accessibility.passMap, this.accessibility.width,this.accessibility.height,this.accessibility.regionID+1) - - var gameState = sharedScript.gameState[PlayerID]; - gameState.ai = this; - - this.runInit(gameState, this.savedEvents); - - // Delete creation events - delete this.savedEvents; - this.savedEvents = []; - } else if ((this.turn + this.player) % 8 == 5) { - - Engine.ProfileStart("Aegis bot"); + Engine.ProfileStart("Aegis bot (player " + this.player +")"); this.playedTurn++; - var gameState = sharedScript.gameState[PlayerID]; - gameState.ai = this; - - if (gameState.getOwnEntities().length === 0){ + if (this.gameState.getOwnEntities().length === 0){ Engine.ProfileStop(); return; // With no entities to control the AI cannot do anything } @@ -175,7 +105,7 @@ QBotAI.prototype.OnUpdate = function(sharedScript) { if (this.pathInfo !== undefined) { var pos = [this.pathInfo.mkeyPos[0] + 150*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 150*Math.sin(this.pathInfo.angle)]; - var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 6, 5);// uncomment for debug:*/, 300000, gameState); + var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 6, 5);// uncomment for debug:*/, 300000, this.gameState); if (path !== undefined && path[1] !== undefined && path[1] == false) { // path is viable and doesn't require boating. // blackzone the last two waypoints. @@ -191,55 +121,52 @@ QBotAI.prototype.OnUpdate = function(sharedScript) { if (this.pathInfo.needboat) { debug ("Assuming this is a water map"); - this.waterMap = true; + this.HQ.waterMap = true; } delete this.pathFinder; delete this.pathInfo; } } - var sfx = "_generic"; - if (gameState.civ() == "athen") - sfx = "_athen" - + var townPhase = this.gameState.townPhase(); + var cityPhase = this.gameState.cityPhase(); // try going up phases. - if (gameState.canResearch("phase_town" + sfx,true) && gameState.getTimeElapsed() > (Config.Economy.townPhase*1000) - && gameState.findResearchers("phase_town" + sfx,true).length != 0 && this.queues.majorTech.totalLength() === 0) { - this.queues.majorTech.addItem(new ResearchPlan(gameState, "phase_town" + sfx,true)); // we rush the town phase. + // TODO: softcode this. + if (this.gameState.canResearch(townPhase,true) && this.gameState.getTimeElapsed() > (Config.Economy.townPhase*1000) && this.gameState.getPopulation() > 40 + && this.gameState.findResearchers(townPhase,true).length != 0 && this.queues.majorTech.length() === 0 + && this.gameState.getOwnEntities().filter(Filters.byClass("Village")).length > 5) + { + this.queueManager.pauseQueue("villager", true); + this.queueManager.pauseQueue("citizenSoldier", true); + this.queueManager.pauseQueue("house", true); + this.queues.majorTech.addItem(new ResearchPlan(this.gameState, townPhase,0,-1,true)); // we rush the town phase. debug ("Trying to reach town phase"); - var nb = gameState.getOwnEntities().filter(Filters.byClass("Village")).length-1; - if (nb < 5) - { - while (nb < 5 && ++nb) - this.queues.house.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_house")); - } - } else if (gameState.canResearch("phase_city_generic",true) && gameState.getTimeElapsed() > (Config.Economy.cityPhase*1000) - && gameState.getOwnEntitiesByRole("worker").length > 85 - && gameState.findResearchers("phase_city_generic", true).length != 0 && this.queues.majorTech.totalLength() === 0) { + } + else if (this.gameState.canResearch(cityPhase,true) && this.gameState.getTimeElapsed() > (Config.Economy.cityPhase*1000) + && this.gameState.getOwnEntitiesByRole("worker").length > 85 + && this.gameState.findResearchers(cityPhase, true).length != 0 && this.queues.majorTech.length() === 0) { debug ("Trying to reach city phase"); - this.queues.majorTech.addItem(new ResearchPlan(gameState, "phase_city_generic")); + this.queues.majorTech.addItem(new ResearchPlan(this.gameState, cityPhase)); } // defcon cooldown - if (this.defcon < 5 && gameState.timeSinceDefconChange() > 20000) + if (this.defcon < 5 && this.gameState.timeSinceDefconChange() > 20000) { this.defcon++; debug ("updefconing to " +this.defcon); - if (this.defcon >= 4 && this.modules.military.hasGarrisonedFemales) - this.modules.military.ungarrisonAll(gameState); + if (this.defcon >= 4 && this.HQ.hasGarrisonedFemales) + this.HQ.ungarrisonAll(this.gameState); } - for (var i in this.modules){ - this.modules[i].update(gameState, this.queues, this.savedEvents); - } - - this.queueManager.update(gameState); - + this.HQ.update(this.gameState, this.queues, this.savedEvents); + + this.queueManager.update(this.gameState); + /* // Use this to debug informations about the metadata. if (this.playedTurn % 10 === 0) { // some debug informations about units. - var units = gameState.getOwnEntities(); + var units = this.gameState.getOwnEntities(); for (var i in units._entities) { var ent = units._entities[i]; @@ -265,7 +192,7 @@ QBotAI.prototype.OnUpdate = function(sharedScript) { //if (this.playedTurn % 5 === 0) - // this.queueManager.printQueues(gameState); + // this.queueManager.printQueues(this.gameState); // Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers // TODO: remove this when the engine gives a random seed @@ -283,7 +210,7 @@ QBotAI.prototype.OnUpdate = function(sharedScript) { this.turn++; }; -QBotAI.prototype.chooseRandomStrategy = function() +AegisBot.prototype.chooseRandomStrategy = function() { // deactivated for now. this.strategy = "normal"; @@ -292,7 +219,7 @@ QBotAI.prototype.chooseRandomStrategy = function() { this.strategy = "rush"; // going to rush. - this.modules.economy.targetNumWorkers = 0; + this.HQ.targetNumWorkers = 0; Config.Economy.townPhase = 480; Config.Economy.cityPhase = 900; Config.Economy.farmsteadStartTime = 600; @@ -302,7 +229,7 @@ QBotAI.prototype.chooseRandomStrategy = function() // TODO: Remove override when the whole AI state is serialised // TODO: this currently is very much equivalent to "rungamestateinit" with a few hacks. Should deserialize/serialize properly someday. -QBotAI.prototype.Deserialize = function(data, sharedScript) +AegisBot.prototype.Deserialize = function(data, sharedScript) { BaseAI.prototype.Deserialize.call(this, data); @@ -345,7 +272,7 @@ QBotAI.prototype.Deserialize = function(data, sharedScript) }; // Override the default serializer -QBotAI.prototype.Serialize = function() +AegisBot.prototype.Serialize = function() { //var ret = BaseAI.prototype.Serialize.call(this); return {}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js b/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js index 0227efcf15..f2a9c5716a 100755 --- a/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js +++ b/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js @@ -6,7 +6,7 @@ * There is a basic support for naval expeditions here. */ -function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , targetFinder) { +function CityAttack(gameState, HQ, uniqueID, targetEnemy, type , targetFinder) { //This is the list of IDs of the units in the plan this.idList=[]; @@ -330,16 +330,16 @@ CityAttack.prototype.addBuildOrder = function(gameState, name, unitStats, resetQ // Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start" // 3 is a special case: no valid path returned. Right now I stop attacking alltogether. -CityAttack.prototype.updatePreparation = function(gameState, militaryManager,events) { +CityAttack.prototype.updatePreparation = function(gameState, HQ,events) { var self = this; if (this.path == undefined || this.target == undefined || this.path === "toBeContinued") { // find our target if (this.target == undefined) { - var targets = this.targetFinder(gameState, militaryManager); + var targets = this.targetFinder(gameState, HQ); if (targets.length === 0) - targets = this.defaultTargetFinder(gameState, militaryManager); + targets = this.defaultTargetFinder(gameState, HQ); if (targets.length !== 0) { debug ("Aiming for " + targets); @@ -365,7 +365,7 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve // Thus I will not do everything at once. // It will probably carry over a few turns but that's no issue. if (this.path === undefined) - this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, this.pathSampling, this.pathWidth,250);//,gameState); + this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, this.pathSampling, this.pathWidth,175);//,gameState); else if (this.path === "toBeContinued") this.path = this.pathFinder.continuePath();//gameState); @@ -383,19 +383,13 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve } else if (this.path[1] === true && this.pathWidth == 2) { // okay so we need a ship. // Basically we'll add it as a new class to train compulsorily, and we'll recompute our path. - if (!gameState.ai.waterMap) + if (!gameState.ai.HQ.waterMap) { debug ("This is actually a water map."); - gameState.ai.waterMap = true; + gameState.ai.HQ.waterMap = true; + return 0; } debug ("We need a ship."); - var stat = { "priority" : 1.1, "minSize" : 2, "targetSize" : 2, "batchSize" : 1, "classes" : ["Warship"], - "interests" : [ ["strength",1], ["cost",1] ] ,"templates" : [] }; - if (type === "superSized") { - this.unitStat["TransportShip"]["minSize"] = 4; - this.unitStat["TransportShip"]["targetSize"] = 4; - } - this.addBuildOrder(gameState, "TransportShip", stat); this.needsShip = true; this.pathWidth = 3; this.pathSampling = 3; @@ -414,6 +408,8 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve this.rallyPoint = this.path[i-1][0]; else this.rallyPoint = this.path[0][0]; + if (i >= 1) + this.path.splice(0,i-1); break; } } @@ -473,25 +469,18 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve } else if (!this.mustStart(gameState)) { // We still have time left to recruit units and do stuffs. - // TODO: check why this can happen instead of resorting to this "hack". - if (this.buildOrder.length === 0 || this.buildOrder[0] === undefined) { - debug ("Ending plan: no build orders"); - return 0; // will abort the plan, should return something else - } - - // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. this.buildOrder.sort(function (a,b) { //}) { var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+a[4]); - aQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); - aQueued += self.queueChamp.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); + aQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); + aQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); a[0] = (a[2].length + aQueued)/a[3]["targetSize"]; var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+b[4]); - bQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); - bQueued += self.queueChamp.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); + bQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); + bQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); b[0] = (b[2].length + bQueued)/b[3]["targetSize"]; a[0] -= a[3]["priority"]; @@ -512,7 +501,7 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); - var queued = this.queue.countTotalQueuedUnitsWithMetadata("special",specialData) + this.queueChamp.countTotalQueuedUnitsWithMetadata("special",specialData) + var queued = this.queue.countQueuedUnitsWithMetadata("special",specialData) + this.queueChamp.countQueuedUnitsWithMetadata("special",specialData) if (queued + inTraining + this.buildOrder[0][2].length <= this.buildOrder[0][3]["targetSize"]) { // find the actual queue we want @@ -521,16 +510,11 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve queue = this.queueChamp; if (this.buildOrder[0][0] < 1 && queue.length() <= 5) { - var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); + var template = HQ.findBestTrainableSoldier(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); // 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) { // TODO: this is a complete hack. - if (this.needsShip && this.buildOrder[0][4] == "TransportShip") { - Engine.ProfileStop(); - Engine.ProfileStop(); - return 0; - } delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. this.buildOrder.splice(0,1); } else { @@ -538,10 +522,10 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve // TODO: this should be plan dependant. if (gameState.getTimeElapsed() > 1800000) max *= 2; - if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"])) - queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData }, this.buildOrder[0][3]["batchSize"],max ) ); + if (gameState.getTemplate(template).hasClass("CitizenSoldier")) + queue.addItem( new TrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); else - queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData }, this.buildOrder[0][3]["batchSize"],max ) ); + queue.addItem( new TrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); } } } @@ -549,9 +533,9 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve if (!this.startedPathing && this.path === undefined) { // find our target - var targets = this.targetFinder(gameState, militaryManager); + var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ - targets = this.defaultTargetFinder(gameState, militaryManager); + targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { this.targetPos = undefined; @@ -632,7 +616,7 @@ CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { if (evenWorkers) { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { - if (ent.getMetadata(PlayerID, "role") != "defence" && !ent.hasClass("Warship")) + if (ent.getMetadata(PlayerID, "role") != "defence") { ent.setMetadata(PlayerID,"role", "attack"); ent.move(self.rallyPoint[0],self.rallyPoint[1]); @@ -642,7 +626,7 @@ CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { } else { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { - if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence" && !ent.hasClass("Warship")) + if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence") ent.move(self.rallyPoint[0],self.rallyPoint[1]); }); } @@ -650,19 +634,19 @@ CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { } // Default target finder aims for conquest critical targets -CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){ +CityAttack.prototype.defaultTargetFinder = function(gameState, HQ){ var targets = undefined; - targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "CivCentre",true); + targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "CivCentre",true); if (targets.length == 0) { - targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "ConquestCritical"); + targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "ConquestCritical"); } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { - targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Town",true); + targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Town",true); } if (targets.length == 0) { - targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Village",true); + targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Village",true); } // no buildings, attack anything conquest critical, even units (it's assuming it won't move). if (targets.length == 0) { @@ -672,7 +656,7 @@ CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){ }; // tupdate -CityAttack.prototype.raidingTargetFinder = function(gameState, militaryManager, Target){ +CityAttack.prototype.raidingTargetFinder = function(gameState, HQ, Target){ var targets = undefined; if (Target == "villager") { @@ -693,14 +677,14 @@ CityAttack.prototype.raidingTargetFinder = function(gameState, militaryManager, } return targets; } else { - return this.defaultTargetFinder(gameState, militaryManager); + return this.defaultTargetFinder(gameState, HQ); } }; // 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 in our IDlist enough units. // now the IDlist units are treated turn by turn -CityAttack.prototype.StartAttack = function(gameState, militaryManager){ +CityAttack.prototype.StartAttack = function(gameState, HQ){ // check we have a target and a path. if (this.targetPos && this.path !== undefined) { @@ -711,11 +695,11 @@ CityAttack.prototype.StartAttack = function(gameState, militaryManager){ var curPos = this.unitCollection.getCentrePosition(); this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "subrole", "walking"); ent.setMetadata(PlayerID, "role", "attack") ;}); - - this.unitCollectionNoWarship = this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))); - this.unitCollectionNoWarship.registerUpdates(); + // optimize our collection now. + this.unitCollection.freeze(); + this.unitCollection.allowQuickIter(); - this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); + this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); this.unitCollection.setStance("aggressive"); this.unitCollection.filter(Filters.byClass("Siege")).setStance("defensive"); @@ -729,7 +713,7 @@ CityAttack.prototype.StartAttack = function(gameState, militaryManager){ }; // Runs every turn after the attack is executed -CityAttack.prototype.update = function(gameState, militaryManager, events){ +CityAttack.prototype.update = function(gameState, HQ, events){ var self = this; Engine.ProfileStart("Update Attack"); @@ -771,9 +755,9 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ { attackedNB++; } - //if (militaryManager.enemyWatchers[attacker.owner()]) { + //if (HQ.enemyWatchers[attacker.owner()]) { //toProcess[attacker.id()] = attacker; - //var armyID = militaryManager.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id()); + //var armyID = HQ.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id()); //armyToProcess[armyID[0]] = armyID[1]; //} } @@ -851,7 +835,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ } if (this.state === "walking"){ - this.position = this.unitCollectionNoWarship.getCentrePosition(); + this.position = this.unitCollection.getCentrePosition(); // probably not too good. if (!this.position) { @@ -880,7 +864,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ this.position5TurnsAgo = this.position; if (this.lastPosition && SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { - this.unitCollectionNoWarship.moveIndiv(this.path[0][0][0], this.path[0][0][1]); + this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. var walls = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("StoneWall"))); var nexttoWalls = false; @@ -902,162 +886,62 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ } // check if our land units are close enough from the next waypoint. - if (SquareVectorDistance(this.unitCollectionNoWarship.getCentrePosition(), this.targetPos) < 7500 || - SquareVectorDistance(this.unitCollectionNoWarship.getCentrePosition(), this.path[0][0]) < 850) { + if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) < 7500 || + SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0][0]) < 650) { if (this.unitCollection.filter(Filters.byClass("Siege")).length !== 0 - && SquareVectorDistance(this.unitCollectionNoWarship.getCentrePosition(), this.targetPos) > 7500 - && SquareVectorDistance(this.unitCollection.filter(Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) > 850) + && SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) >= 7500 + && SquareVectorDistance(this.unitCollection.filter(Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) { } else { + + for (var i = 0; i < this.path.length; ++i) + { + debug ("path waypoint " + i + "," + this.path[i][1] + " at " + uneval(this.path[i][0])); + } + debug ("position is " + this.unitCollection.getCentrePosition()); + // okay so here basically two cases. The first one is "we need a boat at this point". // the second one is "we need to unload at this point". The third is "normal". if (this.path[0][1] !== true) { this.path.shift(); if (this.path.length > 0){ - this.unitCollectionNoWarship.moveIndiv(this.path[0][0][0], this.path[0][0][1]); + this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); } else { debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } - } else if (this.path[0][1] === true) + } else { - // okay we must load our units. - // check if we have some kind of ships. - var ships = this.unitCollection.filter(Filters.byClass("Warship")); - if (ships.length === 0) { + // TODO: make this require an escort later on. + this.path.shift(); + if (this.path.length === 0) { + debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + // we must assume we've arrived at the end of the trail. + this.state = "arrived"; + } else { + /* + var plan = new TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); + this.tpPlanID = plan.ID; + HQ.navalManager.transportPlans.push(plan); + debug ("Transporting over sea"); + this.state = "transporting"; + */ + // TODO: fix this above + //right now we'll abort. Engine.ProfileStop(); - return 0; // abort + return 0; } - - debug ("switch to boarding"); - this.state = "boarding"; } } } - } else if (this.state === "shipping") { - this.position = this.unitCollection.filter(Filters.byClass("Warship")).getCentrePosition(); - - if (!this.lastPosition) - this.lastPosition = [0,0]; - - if (SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { - this.unitCollection.filter(Filters.byClass("Warship")).move(this.path[0][0][0], this.path[0][0][1]); + } else if (this.state === "transporting") { + // check that we haven't finished transporting, ie the plan + if (!HQ.navalManager.checkActivePlan(this.tpPlanID)) + { + this.state = "walking"; } - if (SquareVectorDistance(this.position, this.path[0][0]) < 1600) { - if (this.path[0][1] !== true) - { - this.path.shift(); - if (this.path.length > 0){ - this.unitCollection.filter(Filters.byClass("Warship")).move(this.path[0][0][0], this.path[0][0][1]); - } else { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination, but it's still on the ship…"); - Engine.ProfileStop(); - return 0; // abort - } - } else if (this.path[0][1] === true) - { - debug ("switch to unboarding"); - // we unload - this.state = "unboarding"; - } - } - } else if (this.state === "boarding") { - this.position = this.unitCollectionNoWarship.getCentrePosition(); - - var ships = this.unitCollection.filter(Filters.byClass("Warship")); - if (ships.length === 0) { - Engine.ProfileStop(); - return 0; // abort - } - - var globalPos = this.unitCollectionNoWarship.getCentrePosition(); - var shipPos = ships.getCentrePosition(); - - if (globalPos !== undefined && SquareVectorDistance(globalPos,shipPos) > 800) - { // get them closer - ships.moveIndiv(globalPos[0],globalPos[1]); - this.unitCollectionNoWarship.moveIndiv(shipPos[0],shipPos[1]); - } else { - // okay try to garrison. - var shipsArray = ships.toEntityArray(); - this.unitCollectionNoWarship.forEach(function (ent) { //}){ - if (ent.position()) // if we're not garrisoned - for (var shipId = 0; shipId < shipsArray.length; shipId++) { - if (shipsArray[shipId].garrisoned().length < shipsArray[shipId].garrisonMax()) - { - ent.garrison(shipsArray[shipId]); - break; - } - } - }); - var garrLength = 0; - for (var shipId = 0; shipId < shipsArray.length; shipId++) - garrLength += shipsArray[shipId].garrisoned().length; - - if (garrLength == this.unitCollectionNoWarship.length) { - // okay. - this.path.shift(); - if (this.path.length > 0){ - ships.move(this.path[0][0][0], this.path[0][0][1]); - debug ("switch to shipping"); - this.state = "shipping"; - } else { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); - // we must assume we've arrived at the end of the trail. - this.state = "arrived"; - } - } - } - } else if (this.state === "unboarding") { - - var ships = this.unitCollection.filter(Filters.byClass("Warship")); - if (ships.length === 0) { - Engine.ProfileStop(); - return 0; // abort - } - - this.position = ships.getCentrePosition(); - - // the procedure is pretty simple: we move the ships to the next point and try to unload until all units are over. - // TODO: make it better, like avoiding collisions, and so on. - - if (this.path.length > 1) - ships.move(this.path[1][0][0], this.path[1][0][1]); - - ships.forEach(function (ship) { - ship.unloadAll(); - }); - - var shipsArray = ships.toEntityArray(); - var garrLength = 0; - for (var shipId = 0; shipId < shipsArray.length; shipId++) - garrLength += shipsArray[shipId].garrisoned().length; - - if (garrLength == 0) { - // release the ships - - ships.forEach(function (ent) { - ent.setMetadata(PlayerID, "role",undefined); - ent.setMetadata(PlayerID, "subrole",undefined); - ent.setMetadata(PlayerID, "plan",undefined); - }); - for (var shipId = 0; shipId < shipsArray.length; shipId++) - this.unitCollection.removeEnt(shipsArray[shipId]); - - this.path.shift(); - if (this.path.length > 0){ - this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); - debug ("switch to walking"); - this.state = "walking"; - } else { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); - // we must assume we've arrived at the end of the trail. - this.state = "arrived"; - } - } - } @@ -1094,7 +978,6 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ } if (this.state === "") { - // Units attacked will target their attacker unless they're siege. Then we take another non-siege unit to attack them. for (var key in events) { var e = events[key]; @@ -1106,14 +989,19 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { if (ourUnit.hasClass("Siege")) { - var help = this.unitCollection.filter(Filters.and(Filters.not(Filters.byClass("Siege")),Filters.isIdle())); - if (help.length === 0) - help = this.unitCollection.filter(Filters.not(Filters.byClass("Siege"))); - if (help.length > 0) - help.toEntityArray()[0].attack(attacker.id()); - if (help.length > 1) - help.toEntityArray()[1].attack(attacker.id()); - + var collec = this.unitCollection.filterNearest(ourUnit.position(), 8).filter(Filters.not(Filters.byClass("Siege"))).toEntityArray(); + if (collec.length !== 0) + { + collec[0].attack(attacker.id()); + if (collec.length !== 1) + { + collec[1].attack(attacker.id()); + if (collec.length !== 2) + { + collec[2].attack(attacker.id()); + } + } + } } else { ourUnit.attack(attacker.id()); } @@ -1122,67 +1010,78 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ } } - - var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Unit"))); - var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Structure"))); + var enemyUnits = gameState.getGEC("player-" +this.targetPlayer + "-units"); + var enemyStructures = gameState.getGEC("player-" +this.targetPlayer + "-structures"); if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0) { - this.unitCollUpdateArray = this.unitCollection.toEntityArray(); + this.unitCollUpdateArray = this.unitCollection.toIdArray(); } else { + // some stuffs for locality and speed + var territoryMap = Map.createTerritoryMap(gameState); + var timeElapsed = gameState.getTimeElapsed(); + // Let's check a few units each time we update. Currently 10 - for (var check = 0; check < Math.min(this.unitCollUpdateArray.length,10); check++) + var lgth = Math.min(this.unitCollUpdateArray.length,10); + for (var check = 0; check < lgth; check++) { - var ent = this.unitCollUpdateArray[0]; - - // if the unit is not in my territory, make it move. - var territoryMap = Map.createTerritoryMap(gameState); + var ent = gameState.getEntityById(this.unitCollUpdateArray[0]); + if (!ent) + continue; + var orderData = ent.unitAIOrderData(); + if (orderData.length !== 0) + orderData = orderData[0]; + else + orderData = undefined; + + // if the unit is in my territory, make it move. if (territoryMap.point(ent.position()) - 64 === PlayerID) ent.move(this.targetPos[0],this.targetPos[1]); + // update it. var needsUpdate = false; if (ent.isIdle()) needsUpdate = true; - if (ent.hasClass("Siege") && (!ent.unitAIOrderData() || !ent.unitAIOrderData()["target"] || !gameState.getEntityById(ent.unitAIOrderData()["target"]).hasClass("ConquestCritical")) ) + if (ent.hasClass("Siege") && (!orderData || !orderData["target"] || !gameState.getEntityById(orderData["target"]) || !gameState.getEntityById(orderData["target"]).hasClass("ConquestCritical")) ) needsUpdate = true; - else if (ent.unitAIOrderData() && ent.unitAIOrderData()["target"] && gameState.getEntityById(ent.unitAIOrderData()["target"]).hasClass("Structure")) + else if (!ent.hasClass("Siege") && orderData && orderData["target"] && gameState.getEntityById(orderData["target"]) && gameState.getEntityById(orderData["target"]).hasClass("Structure")) needsUpdate = true; // try to make it attack a unit instead - - if (gameState.getTimeElapsed() - ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") < 10000) + + if (timeElapsed - ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") < 10000) needsUpdate = false; - if (needsUpdate || arrivedthisTurn) + if (needsUpdate === true || arrivedthisTurn) { - ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", gameState.getTimeElapsed()); + ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed); var mStruct = enemyStructures.filter(function (enemy) { //}){ if (!enemy.position() || (enemy.hasClass("StoneWall") && ent.canAttackClass("StoneWall"))) { return false; } - if (SquareVectorDistance(enemy.position(),ent.position()) > 2000) { + if (SquareVectorDistance(enemy.position(),ent.position()) > 3000) { return false; } return true; }); var mUnit; - if (ent.hasClass("Cavalry")) { + if (ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } if (!enemy.hasClass("Support")) return false; - if (SquareVectorDistance(enemy.position(),ent.position()) > 2000) { + if (SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; }); } - if (!ent.hasClass("Cavalry") || mUnit.length === 0) { + if (!(ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) || mUnit.length === 0) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } - if (SquareVectorDistance(enemy.position(),ent.position()) > 2000) { + if (SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; @@ -1211,7 +1110,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ //warn ("Structure " +structb.genericName() + " is worth " +valb); return (valb - vala); }); - + // TODO: handle ballistas here if (mStruct.length !== 0) { if (isGate) ent.attack(mStruct[0].id()); @@ -1226,10 +1125,13 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ ent.move(self.targetPos[0],self.targetPos[1]); } } else { - if (mUnit.length !== 0 && !isGate) { + if (mUnit.length !== 0) { var rand = Math.floor(Math.random() * mUnit.length*0.99); ent.attack(mUnit[(+rand)].id()); //debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); + } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ + //debug ("Units moving to " + uneval(self.targetPos)); + ent.move(self.targetPos[0],self.targetPos[1]); } else if (mStruct.length !== 0) { mStruct.sort(function (structa,structb) { //}){ var vala = structa.costSum(); @@ -1256,22 +1158,18 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ ent.attack(mStruct[+rand].id()); //debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } - } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ - //debug ("Units moving to " + uneval(self.targetPos)); - ent.move(self.targetPos[0],self.targetPos[1]); } } } - - this.unitCollUpdateArray.splice(0,1); } + this.unitCollUpdateArray.splice(0,10); } // updating targets. if (!gameState.getEntityById(this.target.id())) { - var targets = this.targetFinder(gameState, militaryManager); + var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ - targets = this.defaultTargetFinder(gameState, militaryManager); + targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { debug ("Seems like our target has been destroyed. Switching."); diff --git a/binaries/data/mods/public/simulation/ai/aegis/base-manager.js b/binaries/data/mods/public/simulation/ai/aegis/base-manager.js new file mode 100644 index 0000000000..69ad107cbe --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/aegis/base-manager.js @@ -0,0 +1,955 @@ +/* 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, if I ever get any. + -getting good spots for dropsites + -managing dropsite use in the base + > warning HQ if we'll need more space + -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) + */ + +var BaseManager = function() { + this.farmingFields = false; + this.ID = uniqueIDBases++; + + // anchor building: seen as the main building of the base. Needs to have territorial influence + this.anchor = undefined; + // list of IDs of buildings in our base that have a "territory pusher" function. + this.territoryBuildings = []; + + // will tell if we should be considered as a source of X. + this.willGather = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; + + this.isFarming = false; + this.isHunting = true; + + this.constructing = false; + + // vector for iterating, to check one use the HQ map. + this.territoryIndices = []; +}; + +BaseManager.prototype.init = function(gameState, events, unconstructed){ + this.constructing = unconstructed; + // entitycollections + this.units = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Unit"),Filters.byMetadata(PlayerID, "base", this.ID))); + this.buildings = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Structure"),Filters.byMetadata(PlayerID, "base", this.ID))); + + this.workers = this.units.filter(Filters.byMetadata(PlayerID,"role","worker")); + + this.workers.allowQuickIter(); + this.buildings.allowQuickIter(); + this.units.allowQuickIter(); + + this.units.registerUpdates(); + this.buildings.registerUpdates(); + this.workers.registerUpdates(); + + // array of entity IDs, with each being + // { "food" : [close entities, semi-close entities, faraway entities, closeAmount, medianAmount, assignedWorkers collection ] … } (one per resource) + // note that "median amount" also counts the closeAmount. + this.dropsites = { }; + + // TODO: difficulty levels for this? + + // smallRadius is the distance necessary to mark a resource as linked to a dropsite. + this.smallRadius = { 'food':40*40,'wood':45*45,'stone':40*40,'metal':40*40 }; + // medRadius is the maximal distance for a link, albeit one that would still make us want to build a new dropsite. + this.medRadius = { 'food':70*70,'wood':70*70,'stone':80*80,'metal':80*80 }; + // bigRadius is the distance for a weak link, mainly for optimizing search for resources when a DP is depleted. + this.bigRadius = { 'food':70*70,'wood':200*200,'stone':200*200,'metal':200*200 }; +}; + +BaseManager.prototype.assignEntity = function(unit){ + unit.setMetadata(PlayerID, "base", this.ID); + this.units.updateEnt(unit); + this.workers.updateEnt(unit); + this.buildings.updateEnt(unit); + // TODO: immediately assign it some task? + + if (unit.hasClass("Structure") && unit.hasTerritoryInfluence() && this.territoryBuildings.indexOf(unit.id()) === -1) + this.territoryBuildings.push(unit.id()); +}; + +BaseManager.prototype.setAnchor = function(anchorEntity) { + if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence()) + { + warn("Error: Aegis' base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.") + return false; + } + this.anchor = anchorEntity; + this.anchor.setMetadata(PlayerID, "base", this.ID); + this.anchor.setMetadata(PlayerID, "baseAnchor", true); + this.buildings.updateEnt(this.anchor); + + if (this.territoryBuildings.indexOf(this.anchor.id()) === -1) + this.territoryBuildings.push(this.anchor.id()); + return true; +} + +// affects the HQ map. +BaseManager.prototype.initTerritory = function(HQ, gameState) { + if (!this.anchor) + warn ("Error: Aegis tried to initialize the territory of base " + this.ID + " without assigning it an anchor building first"); + var radius = Math.round((this.anchor.territoryInfluenceRadius() / 4.0) * 1.25); + + var LandSize = gameState.sharedScript.accessibility.getRegionSize(this.anchor.position()); + this.accessIndex = gameState.sharedScript.accessibility.getAccessValue(this.anchor.position()); + + if (LandSize < 6500) + { + // We're on a small land, we'll assign all territories in the vicinity. + // there's a slight chance we're on an elongated weird stuff, we'll just pump up a little the radius + radius = Math.round(radius*1.2); + } + var x = Math.round(this.anchor.position()[0]/gameState.cellSize); + var y = Math.round(this.anchor.position()[1]/gameState.cellSize); + + this.territoryIndices = []; + + var width = gameState.getMap().width; + for (var xi = -radius; xi <= radius; ++xi) + for (var yi = -radius; yi <= radius; ++yi) + if (xi*xi+yi*yi < radius*radius && HQ.basesMap.map[(x+xi) + (y+yi)*width] === 0) + { + if (this.accessIndex == gameState.sharedScript.accessibility.landPassMap[x+xi + width*(y+yi)]) + { + this.territoryIndices.push((x+xi) + (y+yi)*width); + HQ.basesMap.map[(x+xi) + (y+yi)*width] = this.ID; + } + } +} + +BaseManager.prototype.initGatheringFunctions = function(HQ, gameState, specTypes) { + // init our gathering functions. + var types = ["food","wood","stone","metal"]; + if (specTypes !== undefined) + type = specTypes; + + var self = this; + var count = 0; + + for (i in types) + { + var type = types[i]; + // TODO: set us as "X" gatherer + + this.buildings.filter(Filters.isDropsite(type)).forEach(function(ent) { self.initializeDropsite(gameState, ent,type) }); + + if (this.getResourceLevel(gameState, type, "all") > 1000) + this.willGather[type] = 1; + } + if (this.willGather["food"] === 0) + { + var needFarm = true; + // Let's check again for food + for (base in HQ.baseManagers) + if (HQ.baseManagers[base].willGather["food"] === 1) + needFarm = false; + if (needFarm) + this.willGather["food"] = 1; + } + debug ("food" + this.willGather["food"]); + debug (this.willGather["wood"]); + debug (this.willGather["stone"]); + debug (this.willGather["metal"]); +} + +BaseManager.prototype.checkEvents = function (gameState, events, queues) { + for (i in events) + { + if (events[i].type == "Destroy") + { + // let's check we haven't lost an important building here. + var evt = events[i]; + if (evt.msg != undefined && !evt.msg.SuccessfulFoundation && evt.msg.entityObj != undefined && evt.msg.metadata !== undefined && evt.msg.metadata[PlayerID] && + evt.msg.metadata[PlayerID]["base"] !== undefined && evt.msg.metadata[PlayerID]["base"] == this.ID) + { + var ent = evt.msg.entityObj; + if (ent.hasTerritoryInfluence()) + this.territoryBuildings.splice(this.territoryBuildings.indexOf(ent.id()),1); + if (ent.resourceDropsiteTypes()) + this.scrapDropsite(gameState, ent); + if (evt.msg.metadata[PlayerID]["baseAnchor"] && evt.msg.metadata[PlayerID]["baseAnchor"] == true) + { + // sounds like we lost our anchor. Let's try rebuilding it. + // TODO: currently the HQ manager sets us as initgathering, we probably ouht to do it + this.anchor = undefined; + + this.constructing = true; // let's switch mode. + this.workers.forEach( function (worker) { + worker.stopMoving(); + }); + if (ent.hasClass("CivCentre")) + { + // TODO: might want to tell the queue manager to pause other stuffs if we are the only base. + queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, 0 , -1,ent.position())); + } else { + // TODO + queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true },0,-1,ent.position())); + } + } + } + } else if (events[i].type == "ConstructionFinished") + { + // let's check we haven't lost an important building here. + var evt = events[i]; + if (evt.msg && evt.msg.newentity) + { + // TODO: we ought to add new resources or do something. + var ent = gameState.getEntityById(evt.msg.newentity); + + if (ent.getMetadata(PlayerID,"base") == this.ID) + { + if(ent.hasTerritoryInfluence()) + this.territoryBuildings.push(ent.id()); + if (ent.resourceDropsiteTypes()) + for (ress in ent.resourceDropsiteTypes()) + this.initializeDropsite(gameState, ent, ent.resourceDropsiteTypes()[ress]); + if (ent.resourceSupplyAmount() && ent.resourceSupplyType()["specific"] == "grain") + this.assignResourceToDP(gameState,ent); + } + } + } else if (events[i].type == "Create") + { + // Checking for resources. + var evt = events[i]; + if (evt.msg && evt.msg.entity) + { + var ent = gameState.getEntityById(evt.msg.entity); + if (ent && ent.resourceSupplyAmount() && ent.owner() == 0) + this.assignResourceToDP(gameState,ent); + } + } + } +}; + +// If no specific dropsite, it'll assign to the closest +BaseManager.prototype.assignResourceToDP = function (gameState, supply, specificDP) { + var type = supply.resourceSupplyType()["generic"]; + if (type == "treasure") + type = supply.resourceSupplyType()["specific"]; + if (!specificDP) + { + var closest = -1; + var dist = Math.min(); + for (i in this.dropsites) + { + var dp = gameState.getEntityById(i); + var distance = SquareVectorDistance(supply.position(), dp.position()); + if (distance < dist && distance < this.bigRadius[type]) + { + closest = dp.id(); + dist = distance; + } + } + if (closest !== -1) + { + supply.setMetadata(PlayerID, "linked-dropsite-close", (dist < this.smallRadius[type]) ); + supply.setMetadata(PlayerID, "linked-dropsite-nearby", (dist < this.medRadius[type]) ); + supply.setMetadata(PlayerID, "linked-dropsite", closest ); + supply.setMetadata(PlayerID, "linked-dropsite-dist", +dist); + } + } + // TODO: ought to recount immediatly. +} + +BaseManager.prototype.initializeDropsite = function (gameState, ent, type) { + var count = 0, farCount = 0; + var self = this; + + var resources = gameState.getResourceSupplies(type); + + // TODO: if we're initing, we should probably remove them anyway. + if (self.dropsites[ent.id()] === undefined || self.dropsites[ent.id()][type] === undefined) { + resources.filter( function (supply) { //}){ + if (!supply.position() || !ent.position()) + return; + var distance = SquareVectorDistance(supply.position(), ent.position()); + + if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { + if (distance < self.bigRadius[type]) { + supply.setMetadata(PlayerID, "linked-dropsite-close", (distance < self.smallRadius[type]) ); + supply.setMetadata(PlayerID, "linked-dropsite-nearby", (distance < self.medRadius[type]) ); + supply.setMetadata(PlayerID, "linked-dropsite", ent.id() ); + supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance); + if(distance < self.smallRadius[type]) + count += supply.resourceSupplyAmount(); + if (distance < self.medRadius[type]) + farCount += supply.resourceSupplyAmount(); + } + } + }); + // This one is both for the nearby and the linked + var filter = Filters.byMetadata(PlayerID, "linked-dropsite", ent.id()); + var collection = resources.filter(filter); + collection.registerUpdates(); + + filter = Filters.byMetadata(PlayerID, "linked-dropsite-close",true); + var collection2 = collection.filter(filter); + collection2.registerUpdates(); + + filter = Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true); + var collection3 = collection.filter(filter); + collection3.registerUpdates(); + + filter = Filters.byMetadata(PlayerID, "linked-to-dropsite", ent.id()); + var WkCollection = this.workers.filter(filter); + WkCollection.registerUpdates(); + + if (!self.dropsites[ent.id()]) + self.dropsites[ent.id()] = {}; + self.dropsites[ent.id()][type] = [collection2,collection3, collection, count, farCount, WkCollection]; + + // TODO: flag us on the SharedScript "type" map. + // TODO: get workers on those resources and do something with them. + } + + if (Config.debug) + { + // Make resources glow wildly + if (type == "food") { + self.dropsites[ent.id()][type][2].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0,0]}); + }); + self.dropsites[ent.id()][type][1].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); + }); + self.dropsites[ent.id()][type][0].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); + }); + } + if (type == "wood") { + self.dropsites[ent.id()][type][2].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0]}); + }); + self.dropsites[ent.id()][type][1].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); + }); + self.dropsites[ent.id()][type][0].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); + }); + } + if (type == "stone") { + self.dropsites[ent.id()][type][2].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0.5,0]}); + }); + self.dropsites[ent.id()][type][1].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); + }); + self.dropsites[ent.id()][type][0].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,10,0]}); + }); + } + if (type == "metal") { + self.dropsites[ent.id()][type][2].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0.5]}); + }); + self.dropsites[ent.id()][type][1].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,2]}); + }); + self.dropsites[ent.id()][type][0].forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,10]}); + }); + } + } +}; + +// completely and "safely" remove a dropsite from our list. +// this also removes any linked resource and so on. +// TODO: should re-add the resources to another dropsite. +BaseManager.prototype.scrapDropsite = function (gameState, ent) { + if (this.dropsites[ent.id()] === undefined) + return true; + + for (i in this.dropsites[ent.id()]) + { + var type = i; + var dp = this.dropsites[ent.id()][i]; + dp[2].forEach(function (supply) { //}){ + supply.deleteMetadata(PlayerID,"linked-dropsite-nearby"); + supply.deleteMetadata(PlayerID,"linked-dropsite-close"); + supply.deleteMetadata(PlayerID,"linked-dropsite"); + supply.deleteMetadata(PlayerID,"linked-dropsite-dist"); + }); + dp[5].forEach(function (worker) { + worker.deleteMetadata(PlayerID,"linked-to-dropsite"); + // TODO: should probably stop the worker or something. + }); + dp = [undefined, undefined, undefined, 0, 0, undefined]; + delete this.dropsites[ent.id()][i]; + } + this.dropsites[ent.id()] = undefined; + delete this.dropsites[ent.id()]; + return true; +}; + +// Returns the position of the best place to build a new dropsite for the specified resource +BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource){ + + var storeHousePlate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); + + // 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. + + var territory = Map.createTerritoryMap(gameState); + + var obstructions = Map.createObstructionMap(gameState,this.accessIndex,storeHousePlate); + obstructions.expandInfluences(); + + // copy the resource map as initialization. + var friendlyTiles = new Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true); + + var DPFoundations = gameState.getOwnFoundations().filter(Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))); + + // TODO: might be better to check dropsites someplace else. + // loop over this in this.terrytoryindices. It's usually a little too much, but it's always enough. + for (var p = 0; p < this.territoryIndices.length; ++p) + { + var j = this.territoryIndices[p]; + friendlyTiles.map[j] *= 1.5; + + // only add where the map is currently not null, ie in our territory and some "Resource" would be close. + // This makes the placement go from "OK" to "human-like". + for (var i in gameState.sharedScript.resourceMaps) + if (friendlyTiles.map[j] !== 0 && i !== "food") + friendlyTiles.map[j] += gameState.sharedScript.resourceMaps[i].map[j]; + + for (var i in this.dropsites) + { + var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; + var dpPos = gameState.getEntityById(i).position(); + if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) + { + friendlyTiles.map[j] = 0; + continue; + } else if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) + friendlyTiles.map[j] /= 2; + } + for (var i in DPFoundations._entities) + { + var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; + var dpPos = gameState.getEntityById(i).position(); + if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) + friendlyTiles.map[j] = 0; + else if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) + friendlyTiles.map[j] /= 2; + } + } + + if (Config.debug) + friendlyTiles.dumpIm("DP_" + resource + "_" + gameState.getTimeElapsed() + ".png"); + + var best = friendlyTiles.findBestTile(2, obstructions); // try to find a spot to place a DP. + var bestIdx = best[0]; + + // tell the dropsite builder we haven't found anything satisfactory. + if (best[1] < 60) + return false; + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + return [x,z]; +}; + +// update the resource level of a dropsite. +BaseManager.prototype.updateDropsite = function (gameState, ent, type) { + if (this.dropsites[ent.id()] === undefined || this.dropsites[ent.id()][type] === undefined) + return undefined; // should initialize it first. + + var count = 0, farCount = 0; + + var resources = gameState.getResourceSupplies(type); + + this.dropsites[ent.id()][type][1].forEach( function (supply) { //}){ + farCount += supply.resourceSupplyAmount(); + }); + this.dropsites[ent.id()][type][0].forEach( function (supply) { //}){ + count += supply.resourceSupplyAmount(); + }); + + this.dropsites[ent.id()][type][3] = count; + this.dropsites[ent.id()][type][4] = farCount; + return true; +}; + +// Updates dropsites. +BaseManager.prototype.updateDropsites = function (gameState) { + // for each dropsite, recalculate + for (i in this.dropsites) + { + for (type in this.dropsites[i]) + { + this.updateDropsite(gameState,gameState.getEntityById(i),type); + } + } + +}; + +// TODO: ought to be cached or something probably +// Returns the number of slots available for workers here. +// we're assuming Max - 3 for metal/stone mines, and 20 for any dropsite that has wood. +// TODO: for wood might want to count the trees too. +// TODO: this returns "future" worker capacity, might want to have a current one. +BaseManager.prototype.getWorkerCapacity = function (gameState, type) { + var count = 0; + if (type == "food") + return 1000000; // TODO: perhaps return something sensible here. + if (type === "stone" || type === "metal") + { + for (id in this.dropsites) + if (this.dropsites[id][type]) + this.dropsites[id][type][1].forEach(function (ent) {// }){ + if (ent.resourceSupplyAmount() > 500) + count += ent.maxGatherers() - 3; + }); + } else if (type === "wood") + { + for (id in this.dropsites) + if (this.dropsites[id][type] && (this.dropsites[id][type][4]) > 1000) + count += Math.min(15, this.dropsites[id][type][4] / 200); + } + return count; +}; + +// TODO: ought to be cached or something probably +// Returns the amount of resource left +BaseManager.prototype.getResourceLevel = function (gameState, type, searchType, threshold) { + var count = 0; + if (searchType == "all") + { + // return all resources in the base area. + gameState.getResourceSupplies(type).filter(Filters.byTerritory(gameState.ai.HQ.basesMap, this.ID)).forEach( function (ent) { //}){ + count += ent.resourceSupplyAmount(); + }); + return count; + } + if (searchType == "dropsites") + { + // for each dropsite, recalculate + for (i in this.dropsites) + if (this.dropsites[i][type] !== undefined) + count += this.dropsites[i][type][4]; + return count; + } + if (searchType == "dropsitesClose") + { + // for each dropsite, recalculate + for (i in this.dropsites) + if (this.dropsites[i][type] !== undefined) + count += this.dropsites[i][type][3]; + return count; + } + if (searchType == "dropsites-dpcount") + { + var seuil = 800; + if (threshold) + seuil = threshold; + // for each dropsite, recalculate + for (i in this.dropsites) + if (this.dropsites[i][type] !== undefined) + { + if (this.dropsites[i][type][4] > seuil) + count++; + } + return count; + } + return 0; +}; + +// check our resource levels and react accordingly +BaseManager.prototype.checkResourceLevels = function (gameState,queues) { + for (type in this.willGather) + { + if (this.willGather[type] === 0) + continue; + if (type !== "food" && gameState.playedTurn % 10 === 4 && this.getResourceLevel(gameState,type, "all") < 200) + this.willGather[type] = 0; // won't gather at all + if (this.willGather[type] === 2) + continue; + var count = this.getResourceLevel(gameState,type, "dropsites"); + if (type == "food") + { + if (!this.isFarming && count < 1600 && queues.field.length === 0) + { + // tell the queue manager we'll be trying to build fields shortly. + for (var i = 0; i < Config.Economy.initialFields;++i) + { + var plan = new ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }); + plan.isGo = function() { return false; }; // don't start right away. + queues.field.addItem(plan); + } + } else if (!this.isFarming && count < 650) + { + for (i in queues.field.queue) + queues.field.queue[i].isGo = function() { return true; }; // start them + this.isFarming = true; + } + if (this.isFarming) + { + var numFarms = 0; + this.buildings.filter(Filters.byClass("Field")).forEach(function (field) { + if (field.resourceSupplyAmount() > 400) + numFarms++; + }); + var numFd = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); + numFarms += numFd; + numFarms += queues.field.countQueuedUnits(); + + // let's see if we need to push new farms. + if (numFd < 2) + if (numFarms < Math.round(this.gatherersByType(gameState, "food").length / 4.6) || numFarms < Math.round(this.workers.length / 15.0)) + queues.field.addItem(new ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + // TODO: refine count to only count my base. + } + } else if (queues.dropsites.length() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0) { + var wantedDPs = Math.ceil(this.gatherersByType(gameState, type).length / 12.0); + var need = wantedDPs - this.getResourceLevel(gameState,type, "dropsites-dpcount",2000); + if (need > 0) + { + var pos = this.findBestDropsiteLocation(gameState, type); + if (!pos) + { + debug ("Found no right position for a " + type + " dropsite, going into \"noSpot\" mode"); + this.willGather[type] = 2; // won't build + // TODO: tell the HQ we'll be needing a new base for this resource, or tell it we've ran out of resource Z. + } else { + debug ("planning new dropsite for " + type); + queues.dropsites.addItem(new ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : this.ID }, 0, -1, pos)); + } + } + } + } + +}; + +// let's return the estimated gather rates. +BaseManager.prototype.getGatherRates = function(gameState, currentRates) { + +}; + +BaseManager.prototype.assignRolelessUnits = function(gameState) { + // TODO: make this cleverer. + var roleless = this.units.filter(Filters.not(Filters.byHasMetadata(PlayerID, "role"))); + var self = this; + roleless.forEach(function(ent) { + if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier")) { + if (ent.hasClass("Cavalry") && !self.isHunting) + return; + 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. +BaseManager.prototype.setWorkersIdleByPriority = function(gameState){ + var self = this; + if (gameState.currentPhase() < 2 && gameState.getTimeElapsed() < 360000) + return; // not in the first phase or the first 6 minutes. + + var types = gameState.ai.queueManager.getAvailableResources(gameState); + + var bestType = ""; + + var avgOverdraft = 0; + + for (i in types.types) + avgOverdraft += types[types.types[i]]; + + avgOverdraft /= 4; + + for (i in types.types) + if (types[types.types[i]] > avgOverdraft + 200 || (types[types.types[i]] > avgOverdraft && avgOverdraft > 200)) + if (this.gatherersByType(gameState,types.types[i]).length > 0) + { + // TODO: perhaps change this? + var nb = 2; + this.gatherersByType(gameState,types.types[i]).forEach( function (ent) { //}){ + if (nb > 0) + { + //debug ("Moving " +ent.id() + " from " + types.types[i]); + nb--; + // TODO: might want to direct assign. + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole","idle"); + } + }); + } + //debug (currentRates); +}; + +// TODO: work on this. +BaseManager.prototype.reassignIdleWorkers = function(gameState) { + + var self = this; + + // Search for idle workers, and tell them to gather resources based on demand + var filter = Filters.or(Filters.byMetadata(PlayerID,"subrole","idle"), Filters.not(Filters.byHasMetadata(PlayerID,"subrole"))); + var idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers); + + if (idleWorkers.length) { + idleWorkers.forEach(function(ent) { + // Check that the worker isn't garrisoned + if (ent.position() === undefined){ + return; + } + if (ent.hasClass("Worker")) { + var types = gameState.ai.HQ.pickMostNeededResources(gameState); + //debug ("assigning " +ent.id() + " to " + types[0]); + ent.setMetadata(PlayerID, "subrole", "gatherer"); + ent.setMetadata(PlayerID, "gather-type", types[0]); + + if (gameState.turnCache["gathererAssignementCache-" + types[0]]) + gameState.turnCache["gathererAssignementCache-" + types[0]]++; + else + gameState.turnCache["gathererAssignementCache-" + types[0]] = 1; + // Okay let's now check we can actually remain here for that + if (self.willGather[types[0]] !== 1) + { + // TODO: if fail, we should probably pick the second most needed resource. + gameState.ai.HQ.switchWorkerBase(gameState, ent, types[0]); + } + } else { + ent.setMetadata(PlayerID, "subrole", "hunter"); + } + }); + } +}; + +BaseManager.prototype.workersBySubrole = function(gameState, subrole) { + return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true); +}; + +BaseManager.prototype.gatherersByType = function(gameState, type) { + return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, 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. +BaseManager.prototype.pickBuilders = function(gameState, number) { + var collec = new EntityCollection(gameState.sharedScript); + // TODO: choose better. + var workers = this.workers.filter(Filters.not(Filters.byClass("Cavalry"))).toEntityArray(); + workers.sort(function (a,b) { + var vala = 0, valb = 0; + if (a.getMetadata(PlayerID,"subrole") == "builder") + vala = 100; + if (b.getMetadata(PlayerID,"subrole") == "builder") + valb = 100; + if (a.getMetadata(PlayerID,"plan") != undefined) + vala = -100; + if (b.getMetadata(PlayerID,"plan") != undefined) + valb = -100; + return a < b + }); + for (var i = 0; i < number; ++i) + { + workers[i].stopMoving(); + workers[i].setMetadata(PlayerID, "subrole","idle"); + collec.addEnt(workers[i]); + } + return collec; +} + +BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { + // 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. + + var self = this; + + var foundations = this.buildings.filter(Filters.and(Filters.isFoundation(),Filters.not(Filters.byClass("Field")))).toEntityArray(); + var damagedBuildings = this.buildings.filter(function (ent) { if (ent.needsRepair() && ent.getMetadata(PlayerID, "plan") == undefined) { return true; } return false; }).toEntityArray(); + + // Check if nothing to build + if (!foundations.length && !damagedBuildings.length){ + return; + } + var workers = this.workers.filter(Filters.not(Filters.byClass("Cavalry"))); + var builderWorkers = this.workersBySubrole(gameState, "builder"); + var idleBuilderWorkers = this.workersBySubrole(gameState, "builder").filter(Filters.isIdle()); + + // if we're constructing and we have the foundations to our base anchor, only try building that. + if (this.constructing == true && this.buildings.filter(Filters.and(Filters.isFoundation(), Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0) + { + foundations = this.buildings.filter(Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray(); + var tID = foundations[0].id(); + workers.forEach(function (ent) { //}){ + var target = ent.getMetadata(PlayerID, "target-foundation"); + if (target && target != tID) + { + ent.stopMoving(); + ent.setMetadata(PlayerID, "target-foundation", tID); + } + }); + } + + if (workers.length < 2) + { + var noobs = gameState.ai.HQ.bulkPickWorkers(gameState, this.ID, 2); + if(noobs) + { + noobs.forEach(function (worker) { //}){ + worker.setMetadata(PlayerID,"base", self.ID); + worker.setMetadata(PlayerID,"subrole", "builder"); + workers.updateEnt(worker); + builderWorkers.updateEnt(worker); + idleBuilderWorkers.updateEnt(worker); + }); + } + } + var addedWorkers = 0; + + var maxTotalBuilders = Math.ceil(workers.length * 0.15); + if (this.constructing == true && maxTotalBuilders < 15) + maxTotalBuilders = 15; + + for (var i in foundations) { + var target = foundations[i]; + // Removed: sometimes the AI would not notice it has empty unbuilt fields + //if (target._template.BuildRestrictions.Category === "Field") + // continue; // we do not build fields + + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + + var targetNB = Config.Economy.targetNumBuilders; // TODO: dynamic that. + if (target.hasClass("CivCentre") || target.buildTime() > 150 || target.hasClass("House")) + targetNB *= 2; + if (target.getMetadata(PlayerID, "baseAnchor") == true) + targetNB = 15; + + if (assigned < targetNB) { + if (builderWorkers.length - idleBuilderWorkers.length + addedWorkers < maxTotalBuilders) { + + var addedToThis = 0; + + idleBuilderWorkers.forEach(function(ent) { + if (ent.position() && SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB) + { + addedWorkers++; + addedToThis++; + ent.setMetadata(PlayerID, "target-foundation", target.id()); + } + }); + if (assigned + addedToThis < targetNB) + { + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB - assigned - addedToThis); + nearestNonBuilders.forEach(function(ent) { + addedWorkers++; + addedToThis++; + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", "builder"); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + } + } + } + } + // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed. + for (var i in damagedBuildings) { + var target = damagedBuildings[i]; + if (gameState.defcon() < 5) { + if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) { + continue; + } + } else if (noRepair && !target.hasClass("CivCentre")) + continue; + + var territory = Map.createTerritoryMap(gameState); + if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID) + continue; + + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + if (assigned < this.targetNumBuilders/3) { + if (builderWorkers.length + addedWorkers < this.targetNumBuilders*2) { + + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); + if (gameState.defcon() < 5) + nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders/3 - assigned); + + nearestNonBuilders.forEach(function(ent) { + ent.stopMoving(); + addedWorkers++; + ent.setMetadata(PlayerID, "subrole", "builder"); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + } + } + } +}; + +BaseManager.prototype.update = function(gameState, queues, events) { + Engine.ProfileStart("Base update - base " + this.ID); + var self = this; + + this.updateDropsites(gameState); + this.checkResourceLevels(gameState, queues); + + Engine.ProfileStart("Assign builders"); + this.assignToFoundations(gameState); + Engine.ProfileStop() + +// if (!this.constructing) +// { + if (gameState.ai.playedTurn % 2 === 0) + this.setWorkersIdleByPriority(gameState); + + this.assignRolelessUnits(gameState); + + /*Engine.ProfileStart("Swap Workers"); + var gathererGroups = {}; + gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ }){ + if (ent.hasClass("Cavalry")) + return; + var key = uneval(ent.resourceGatherRates()); + if (!gathererGroups[key]){ + gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; + } + if (ent.getMetadata(PlayerID, "gather-type") in gathererGroups[key]){ + gathererGroups[key][ent.getMetadata(PlayerID, "gather-type")].push(ent); + } + }); + for (var i in gathererGroups){ + for (var j in gathererGroups){ + var a = eval(i); + var b = eval(j); + if (a !== undefined && b !== undefined) + if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 + && gathererGroups[j]["food"].length > 0){ + for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ + gathererGroups[i]["wood"][k].setMetadata(PlayerID, "gather-type", "food"); + gathererGroups[j]["food"][k].setMetadata(PlayerID, "gather-type", "wood"); + } + } + } + } + Engine.ProfileStop();*/ + + // should probably be last to avoid reallocations of units that would have done stuffs otherwise. + Engine.ProfileStart("Assigning Workers"); + this.reassignIdleWorkers(gameState); + Engine.ProfileStop(); +// } + + // TODO: do this incrementally a la defence.js + Engine.ProfileStart("Run Workers"); + this.workers.forEach(function(ent) { + if (!ent.getMetadata(PlayerID, "worker-object")) + ent.setMetadata(PlayerID, "worker-object", new Worker(ent)); + ent.getMetadata(PlayerID, "worker-object").update(self, gameState); + }); + Engine.ProfileStop(); + + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/config.js b/binaries/data/mods/public/simulation/ai/aegis/config.js index 3cf7f93f4a..8103480004 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/config.js +++ b/binaries/data/mods/public/simulation/ai/aegis/config.js @@ -3,16 +3,22 @@ var baseConfig = { "Military" : { "fortressLapseTime" : 540, // Time to wait between building 2 fortresses "defenceBuildingTime" : 600, // Time to wait before building towers or fortresses - "attackPlansStartTime" : 0 // time to wait before attacking. Start as soon as possible (first barracks) + "attackPlansStartTime" : 0, // time to wait before attacking. Start as soon as possible (first barracks) + "techStartTime" : 120, // time to wait before teching. Will only start after town phase so it's irrelevant. + "popForBarracks1" : 15, + "popForBarracks2" : 95, + "timeForBlacksmith" : 900, }, "Economy" : { "townPhase" : 180, // time to start trying to reach town phase (might be a while after. Still need the requirements + ress ) - "cityPhase" : 540, // time to start trying to reach city phase - "farmsteadStartTime" : 400, // Time to wait before building a farmstead. + "cityPhase" : 840000, // time to start trying to reach city phase + "popForMarket" : 80, + "popForFarmstead" : 45, "dockStartTime" : 240, // Time to wait before building the dock - "techStartTime" : 600, // time to wait before teching. - "targetNumBuilders" : 1.5, // Base number of builders per foundation. Later updated, but this remains a multiplier. - "femaleRatio" : 0.6 // percent of females among the workforce. + "techStartTime" : 0, // time to wait before teching. + "targetNumBuilders" : 1.5, // Base number of builders per foundation. + "femaleRatio" : 0.4, // percent of females among the workforce. + "initialFields" : 2 }, // Note: attack settings are set directly in attack_plan.js @@ -49,19 +55,20 @@ var baseConfig = { // qbot "priorities" : { // Note these are dynamic, you are only setting the initial values - "house" : 200, - "citizenSoldier" : 70, - "villager" : 55, - "economicBuilding" : 70, + "house" : 350, + "villager" : 40, + "citizenSoldier" : 60, + "ships" : 70, + "economicBuilding" : 90, "dropsites" : 120, - "field" : 1000, - "militaryBuilding" : 90, + "field" : 500, + "militaryBuilding" : 110, "defenceBuilding" : 70, - "majorTech" : 400, - "minorTech" : 40, - "civilCentre" : 10000 // will hog all resources + "majorTech" : 700, + "minorTech" : 50, + "civilCentre" : 400 }, - "difficulty" : 2, // for now 2 is "hard", ie default. 1 is normal, 0 is easy. 3 is very hard + "difficulty" : 2, // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard. "debug" : false }; @@ -74,50 +81,35 @@ var Config = { // changing settings based on difficulty. if (Config.difficulty === 1) { - Config["Military"] = { - "fortressLapseTime" : 900, - "defenceBuildingTime" : 720, - "attackPlansStartTime" : 1200 - }; - Config["Economy"] = { - "townPhase" : 360, - "cityPhase" : 900, - "farmsteadStartTime" : 600, - "dockStartTime" : 240, - "techStartTime" : 1320, - "targetNumBuilders" : 2, - "femaleRatio" : 0.5, - "targetNumWorkers" : 110 // should not make more than 2 barracks. - }; - Config["Defence"] = { - "defenceRatio" : 4.0, - "armyCompactSize" : 700, - "armyBreakawaySize" : 900 - }; - } else if (Config.difficulty === 0) + Config.Military.defenceBuildingTime = 1200; + Config.Military.attackPlansStartTime = 960; + Config.Military.popForBarracks1 = 35; + Config.Military.popForBarracks2 = 150; // shouldn't reach it + Config.Military.popForBlacksmith = 150; // shouldn't reach it + + Config.Economy.cityPhase = 1800; + Config.Economy.popForMarket = 80; + Config.Economy.techStartTime = 600; + Config.Economy.femaleRatio = 0.6; + Config.Economy.initialFields = 1; + // Config.Economy.targetNumWorkers will be set by AI scripts. + } + else if (Config.difficulty === 0) { - Config["Military"] = { - "fortressLapseTime" : 1000000, // never - "defenceBuildingTime" : 900, - "attackPlansStartTime" : 120000 // never - }; - Config["Economy"] = { - "townPhase" : 480, - "cityPhase" : 1200, - "farmsteadStartTime" : 1200, - "dockStartTime" : 240, - "techStartTime" : 600000, // never - "targetNumBuilders" : 1, - "femaleRatio" : 0.0, // makes us slower, but also less sucky at defending so it's still fun to attack it. - "targetNumWorkers" : 70 - }; - Config["Defence"] = { - "defenceRatio" : 2.0, - "armyCompactSize" : 700, - "armyBreakawaySize" : 900 - }; + Config.Military.defenceBuildingTime = 450; + Config.Military.attackPlansStartTime = 9600000; // never + Config.Military.popForBarracks1 = 60; + Config.Military.popForBarracks2 = 150; // shouldn't reach it + Config.Military.popForBlacksmith = 150; // shouldn't reach it + + Config.Economy.cityPhase = 240000; + Config.Economy.popForMarket = 200; + Config.Economy.techStartTime = 1800; + Config.Economy.femaleRatio = 0.2; + Config.Economy.initialFields = 1; + // Config.Economy.targetNumWorkers will be set by AI scripts. } } }; -Config.__proto__ = baseConfig; \ No newline at end of file +Config.__proto__ = baseConfig; diff --git a/binaries/data/mods/public/simulation/ai/aegis/data.json b/binaries/data/mods/public/simulation/ai/aegis/data.json index 211b5adad7..4aeb576372 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/data.json +++ b/binaries/data/mods/public/simulation/ai/aegis/data.json @@ -1,6 +1,6 @@ { "name": "Aegis Bot", "description": "Wraitii's improvement of qBot. It is more reliable and generally a better player. Note that it doesn't support saved games yet, and there may be other bugs. Please report issues to Wildfire Games (see the link in the main menu).", - "constructor": "QBotAI", + "constructor": "AegisBot", "useShared": true } diff --git a/binaries/data/mods/public/simulation/ai/aegis/defence.js b/binaries/data/mods/public/simulation/ai/aegis/defence.js index d55d613ff1..96d1cb474f 100755 --- a/binaries/data/mods/public/simulation/ai/aegis/defence.js +++ b/binaries/data/mods/public/simulation/ai/aegis/defence.js @@ -48,7 +48,7 @@ function Defence(){ // 1: Huge army in the base, outnumbering us. -Defence.prototype.update = function(gameState, events, militaryManager){ +Defence.prototype.update = function(gameState, events, HQ){ Engine.ProfileStart("Defence Manager"); @@ -80,13 +80,13 @@ Defence.prototype.update = function(gameState, events, militaryManager){ this.territoryMap = Map.createTerritoryMap(gameState); // used by many func // First step: we deal with enemy armies, those are the highest priority. - this.defendFromEnemies(gameState, events, militaryManager); + this.defendFromEnemies(gameState, events, HQ); // second step: we loop through messages, and sort things as needed (dangerous buildings, attack by animals, ships, lone units, whatever). // TODO : a lot. - this.MessageProcess(gameState,events,militaryManager); + this.MessageProcess(gameState,events,HQ); - this.DealWithWantedUnits(gameState,events,militaryManager); + this.DealWithWantedUnits(gameState,events,HQ); /* var self = this; @@ -138,7 +138,7 @@ Defence.prototype.evaluateArmies = function(gameState, armies) { }*/ // Incorporates an entity in an army. If no army fits, it creates a new one around this one. // an army is basically an entity collection. -Defence.prototype.armify = function(gameState, entity, militaryManager, minNBForArmy) { +Defence.prototype.armify = function(gameState, entity, HQ, minNBForArmy) { if (entity.position() === undefined) return; if (this.enemyArmy[entity.owner()] === undefined) @@ -160,10 +160,10 @@ Defence.prototype.armify = function(gameState, entity, militaryManager, minNBFor } } } - if (militaryManager) + if (HQ) { var self = this; - var close = militaryManager.enemyWatchers[entity.owner()].enemySoldiers.filter(Filters.byDistance(entity.position(), self.armyCompactSize)); + var close = HQ.enemyWatchers[entity.owner()].enemySoldiers.filter(Filters.byDistance(entity.position(), self.armyCompactSize)); if (!minNBForArmy || close.length >= minNBForArmy) { // if we're here, we need to create an army for it, and freeze it to make sure no unit will be added automatically @@ -223,7 +223,7 @@ Defence.prototype.reevaluateEntity = function(gameState, entity) { } // This deals with incoming enemy armies, setting the defcon if needed. It will take new soldiers, and assign them to attack // TODO: still is still pretty dumb, it could use improvements. -Defence.prototype.defendFromEnemies = function(gameState, events, militaryManager) { +Defence.prototype.defendFromEnemies = function(gameState, events, HQ) { var self = this; // New, faster system will loop for enemy soldiers, and also females on occasions ( TODO ) @@ -264,10 +264,10 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage // When it's finished it'll start over. for (var enemyID in this.enemyArmy) { - //this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].getAllEnemySoldiers(); + //this.enemyUnits[enemyID] = HQ.enemyWatchers[enemyID].getAllEnemySoldiers(); if (this.enemyUnits[enemyID] === undefined || this.enemyUnits[enemyID].length === 0) { - this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].enemySoldiers.toEntityArray(); + this.enemyUnits[enemyID] = HQ.enemyWatchers[enemyID].enemySoldiers.toEntityArray(); } else { // we have some units still to check in this array. Check 15 (TODO: DIFFLEVEL) // Note: given the way memory works, if the entity has been recently deleted, its reference may still exist. @@ -283,7 +283,7 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage } else { var dangerous = this.evaluateEntity(gameState, this.enemyUnits[enemyID][0]); if (dangerous) - this.armify(gameState, this.enemyUnits[enemyID][0], militaryManager,2); + this.armify(gameState, this.enemyUnits[enemyID][0], HQ,2); this.enemyUnits[enemyID].splice(0,1); } } else if (this.enemyUnits[enemyID].length > 0 && gameState.getEntityById(this.enemyUnits[enemyID][0].id()) === undefined) @@ -379,8 +379,8 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage defender.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; }); - militaryManager.ungarrisonAll(gameState); - militaryManager.unpauseAllPlans(gameState); + HQ.ungarrisonAll(gameState); + HQ.unpauseAllPlans(gameState); return; } else if (this.nbAttackers === 0 && this.nbDefenders !== 0) { // Release all our units @@ -393,12 +393,12 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage defender.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; }); - militaryManager.ungarrisonAll(gameState); - militaryManager.unpauseAllPlans(gameState); + HQ.ungarrisonAll(gameState); + HQ.unpauseAllPlans(gameState); return; } if ( (this.nbDefenders < 4 && this.nbAttackers >= 5) || this.nbDefenders === 0) { - militaryManager.ungarrisonAll(gameState); + HQ.ungarrisonAll(gameState); } //debug ("total number of attackers:"+ this.nbAttackers); @@ -449,7 +449,7 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage //debug ("nonDefenders.length "+ nonDefenders.length); if (gameState.defcon() > 3) - militaryManager.unpauseAllPlans(gameState); + HQ.unpauseAllPlans(gameState); if ( (nonDefenders.length + this.nbDefenders > newEnemies.length + this.nbAttackers) || this.nbDefenders + nonDefenders.length < 4) @@ -467,9 +467,9 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage /* if (gameState.defcon() < 2 && (this.nbAttackers-this.nbDefenders) > 15) { - militaryManager.pauseAllPlans(gameState); + HQ.pauseAllPlans(gameState); } else if (gameState.defcon() < 3 && this.nbDefenders === 0 && newEnemies.length === 0) { - militaryManager.ungarrisonAll(gameState); + HQ.ungarrisonAll(gameState); }*/ // A little sorting to target sieges/champions first. @@ -549,7 +549,7 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage // successfully sorted defs.forEach(function (defender) { //}){ if (defender.getMetadata(PlayerID, "plan") != undefined && (gameState.defcon() < 4 || defender.getMetadata(PlayerID,"subrole") == "walking")) - militaryManager.pausePlan(gameState, defender.getMetadata(PlayerID, "plan")); + HQ.pausePlan(gameState, defender.getMetadata(PlayerID, "plan")); //debug ("Against " +enemy.id() + " Assigning " + defender.id()); if (defender.getMetadata(PlayerID, "role") == "worker" || defender.getMetadata(PlayerID, "role") == "attack") defender.setMetadata(PlayerID, "formerrole", defender.getMetadata(PlayerID, "role")); @@ -605,7 +605,7 @@ Defence.prototype.defendFromEnemies = function(gameState, events, militaryManage // this processes the attackmessages // So that a unit that gets attacked will not be completely dumb. // warning: huge levels of indentation coming. -Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { +Defence.prototype.MessageProcess = function(gameState,events, HQ) { var self = this; for (var key in events){ @@ -677,7 +677,7 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { if (this.reevaluateEntity(gameState, attacker)) { var position = attacker.position(); - var close = militaryManager.enemyWatchers[attacker.owner()].enemySoldiers.filter(Filters.byDistance(position, self.armyCompactSize)); + var close = HQ.enemyWatchers[attacker.owner()].enemySoldiers.filter(Filters.byDistance(position, self.armyCompactSize)); if (close.length > 2 || ourUnit.hasClass("Support") || attacker.hasClass("Siege")) { @@ -736,7 +736,7 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { }; // nice sets of closing brackets, isn't it? // At most, this will put defcon to 4 -Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryManager) { +Defence.prototype.DealWithWantedUnits = function(gameState, events, HQ) { //if (gameState.defcon() < 3) // return; diff --git a/binaries/data/mods/public/simulation/ai/aegis/economy.js b/binaries/data/mods/public/simulation/ai/aegis/economy.js deleted file mode 100644 index b7ae7db866..0000000000 --- a/binaries/data/mods/public/simulation/ai/aegis/economy.js +++ /dev/null @@ -1,1337 +0,0 @@ -/* Economy Manager - * Deals with anything economic. Worker logic is in worker.js. - */ - -var EconomyManager = function() { - this.targetNumBuilders = Config.Economy.targetNumBuilders; // number of workers we want building stuff - this.targetNumFields = 3; // initial setting only. - - // Used by the QueueManager to determine future needs. - this.baseNeed = {}; - this.baseNeed["food"] = 300; // only really early. - this.baseNeed["wood"] = 130; - this.baseNeed["stone"] = 0; - this.baseNeed["metal"] = 0; - - // see rePrioritize() for more info - this.lastStatG = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0}; // resource collecting stats: gathered - this.lastStatU = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0}; // resource collecting stats: used - - this.dockStartTime = Config.Economy.dockStartTime * 1000; - this.farmsteadStartTime = Config.Economy.farmsteadStartTime * 1000; - this.techStartTime = Config.Economy.techStartTime * 1000; - - this.dockFailed = false; // sanity check - - // A few notes about these maps. They're updated by checking for "create" and "destroy" events. - // They are also updated by dropsites, as resources that are seen as part of a dropsite are - // removed from the map. The AI otherwise tries to build tons of dropsites next to each other. - // It might actually be better to create when needed over a few frames. Dunno. - this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal - this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. - - this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of - //turns before trying to reassign them. - - // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. - this.femaleRatio = Config.Economy.femaleRatio; - - this.farmingFields = false; - - this.dropsiteNumbers = {"wood": 1, "stone": 0.5, "metal": 0.5}; -}; -// More initialisation for stuff that needs the gameState -EconomyManager.prototype.init = function(gameState, events){ - if (Config.Economy.targetNumWorkers) - this.targetNumWorkers = Config.Economy.targetNumWorkers; - else if (this.targetNumWorkers === undefined) - this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.45), 1); - - var availableRess = 0; - var availableRessFood = 0; - availableRess += gameState.ai.queueManager.getAvailableResources(gameState,false).toInt(); - availableRessFood += gameState.ai.queueManager.getAvailableResources(gameState,false)["food"]; - - var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID)); - ents = ents.filter(Filters.byClass("CivCentre")).toEntityArray(); - if (ents.length > 0) - { - gameState.getResourceSupplies("food").forEach( function (ent) { - if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), ents[0].position()) < 5000) { - availableRess += ent.resourceSupplyMax(); - availableRessFood += ent.resourceSupplyMax(); - } - }); - gameState.getResourceSupplies("stone").forEach( function (ent) { - if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), ents[0].position()) < 5000) - availableRess += ent.resourceSupplyMax(); - }); - gameState.getResourceSupplies("metal").forEach( function (ent) { - if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), ents[0].position()) < 5000) - availableRess += ent.resourceSupplyMax(); - }); - gameState.getResourceSupplies("wood").forEach( function (ent) { - if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), ents[0].position()) < 5000) - availableRess += ent.resourceSupplyMax(); - }); - } - if (availableRess > 2000) - this.fastStart = true; - - if (availableRessFood < 500) - { - // this is going to be slow. - this.fastStart = false; - Config.Economy.townPhase += 60; // add one minute - this.baseNeed["wood"] = 150; - } - - // initialize once all the resource maps. - this.updateResourceMaps(gameState, events); - this.updateResourceConcentrations(gameState,"food"); - this.updateResourceConcentrations(gameState,"wood"); - this.updateResourceConcentrations(gameState,"stone"); - this.updateResourceConcentrations(gameState,"metal"); - - this.reassignIdleWorkers(gameState); -}; - -// okay, so here we'll create both females and male workers. -// We'll try to keep close to the "ratio" defined atop. -// Choice of citizen soldier is a bit messy. -// Before having 100 workers it focuses on speed, cost, and won't choose units that cost stone/metal -// After 100 it just picks the strongest; -// TODO: This should probably be changed to favor a more mixed approach for better defense. -// (or even to adapt based on estimated enemy strategy). -// Also deals with setting the watned numbers of dropsites and fields since it's practical, -// this function should probably be renamed. -EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) { - // Count the workers in the world and in progress - var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen")); - numFemales += queues.villager.countTotalQueuedUnits(); - - // counting the workers that aren't part of a plan - var numWorkers = 0; - gameState.getOwnEntities().forEach (function (ent) { - if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") == undefined) - numWorkers++; - }); - var numInTraining = 0; - gameState.getOwnTrainingFacilities().forEach(function(ent) { - ent.trainingQueue().forEach(function(item) { - if (item.metadata && item.metadata.role == "worker" && item.metadata.plan == undefined) - numWorkers += item.count; - numInTraining += item.count; - }); - }); - var numQueued = queues.villager.countTotalQueuedUnits() + queues.citizenSoldier.countTotalQueuedUnits(); - var numTotal = numWorkers + numQueued; - - this.targetNumFields = numWorkers/10.0; // 5 workers per field max. - - // ought to refine this. - if ((gameState.ai.playedTurn+2) % 3 === 0) { - this.dropsiteNumbers = {"wood": Math.ceil(numWorkers/25)/2, "stone": Math.ceil(numWorkers/30)/2, "metal": Math.ceil(numWorkers/20)/2}; - if (numWorkers < 30) - { - this.dropsiteNumbers["wood"] -= 0.5; - this.dropsiteNumbers["metal"] -= 0.5; - this.dropsiteNumbers["stone"] -= 0.5; - } - } - - //debug (numTotal + "/" +this.targetNumWorkers + ", " +numFemales +"/" +numTotal); - - // If we have too few, train more - // should plan enough to always have females… - if (numTotal < this.targetNumWorkers && numQueued < 15 && ((queues.villager.length() < 2 && queues.citizenSoldier.length() < 2) || gameState.currentPhase() !== 1) && (numInTraining) < 15) { - var template = gameState.applyCiv("units/{civ}_support_female_citizen"); - var size = Math.min(Math.ceil(gameState.getTimeElapsed() / 20000),5); - if (numFemales/numTotal > this.femaleRatio && (gameState.getTimeElapsed() > 150*1000 || (this.fastStart && gameState.getTimeElapsed() > 60*1000))) { - if (numTotal < 100) - template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]); - else - template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["strength",1] ]); - if (!template) - template = gameState.applyCiv("units/{civ}_support_female_citizen"); - else - size = Math.min(Math.ceil(gameState.getTimeElapsed() / 60000),5); - } - - if ((gameState.getTimeElapsed() < 60000 && gameState.ai.queueManager.getAvailableResources(gameState)["food"] > 250) || this.fastStart) - size = 5; - - if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) - queues.villager.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" }, size, size )); - else - queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" }, size, size )); - } -}; - -// Tries to research any available tech -// Only one at once. Also does military tech (selection is completely random atm) -// TODO: Lots, lots, lots here. -EconomyManager.prototype.tryResearchTechs = function(gameState, queues) { - if (queues.minorTech.totalLength() === 0) - { - var possibilities = gameState.findAvailableTech(); - if (possibilities.length === 0) - return; - // randomly pick one. No worries about pairs in that case. - var p = Math.floor((Math.random()*possibilities.length)); - queues.minorTech.addItem(new ResearchPlan(gameState, possibilities[p][0])); - } -} - -// picks the best template based on parameters and classes -// Similar to the one used in the Military manager but not quite. -EconomyManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { - var units = gameState.findTrainableUnits(classes); - - if (units.length === 0) - return undefined; - - units.sort(function(a, b) { //}) { - var aDivParam = 0, bDivParam = 0; - var aTopParam = 0, bTopParam = 0; - for (var i in parameters) { - var param = parameters[i]; - - if (param[0] == "base") { - aTopParam = param[1]; - bTopParam = param[1]; - } - if (param[0] == "strength") { - aTopParam += getMaxStrength(a[1]) * param[1]; - bTopParam += getMaxStrength(b[1]) * param[1]; - } - if (param[0] == "speed") { - aTopParam += a[1].walkSpeed() * param[1]; - bTopParam += b[1].walkSpeed() * param[1]; - } - - if (param[0] == "cost") { - aDivParam += a[1].costSum() * param[1]; - bDivParam += b[1].costSum() * param[1]; - } - // requires a third parameter which is the resource - if (param[0] == "costsResource") { - if (a[1].cost()[param[2]]) - aTopParam *= param[1]; - if (b[1].cost()[param[2]]) - bTopParam *= param[1]; - } - } - return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); - }); - return units[0][0]; -}; - - - -// Pick the resource which most needs another worker -EconomyManager.prototype.pickMostNeededResources = function(gameState) { - var self = this; - - // Find what resource type we're most in need of - if (!gameState.turnCache["gather-weights-calculated"]){ - this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState,this); - gameState.turnCache["gather-weights-calculated"] = true; - } - - var numGatherers = {}; - for (var type in this.gatherWeights){ - numGatherers[type] = gameState.updatingCollection("workers-gathering-" + type, - Filters.byMetadata(PlayerID, "gather-type", type)).length;//, gameState.getOwnEntitiesByRole("worker")).length; - } - //var totalWeight = numGatherers[a].length/gameState.getOwnEntitiesByRole("worker")).length; - var types = Object.keys(this.gatherWeights); - - types.sort(function(a, b) { - // Prefer fewer gatherers (divided by weight) - var va = numGatherers[a] / (self.gatherWeights[a]+1); - var vb = numGatherers[b] / (self.gatherWeights[b]+1); - if (self.gatherWeights[a] === 0) - va = 10000; - if (self.gatherWeights[b] === 0) - vb = 10000; - return va-vb; - }); - return types; -}; - -EconomyManager.prototype.reassignRolelessUnits = function(gameState) { - //TODO: Move this out of the economic section - var roleless = gameState.getOwnEntitiesByRole(undefined); - - roleless.forEach(function(ent) { - if ((ent.hasClass("Worker") || ent.hasClass("CitizenSoldier")) && !ent.getMetadata(PlayerID, "stoppedHunting")) { - 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. -EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){ - this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState,this); - - var numGatherers = {}; - var totalGatherers = 0; - var totalWeight = 0; - for ( var type in this.gatherWeights){ - numGatherers[type] = 0; - totalWeight += this.gatherWeights[type]; - } - - gameState.getOwnEntitiesByRole("worker").forEach(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") === "gatherer"){ - numGatherers[ent.getMetadata(PlayerID, "gather-type")] += 1; - totalGatherers += 1; - } - }); - - for ( var type in this.gatherWeights){ - var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight)); - if (allocation < numGatherers[type]){ - var numToTake = numGatherers[type] - allocation; - gameState.getOwnEntitiesByRole("worker").forEach(function(ent) { - if (ent.getMetadata(PlayerID, "subrole") === "gatherer" && ent.getMetadata(PlayerID, "gather-type") === type && numToTake > 0){ - ent.setMetadata(PlayerID, "subrole", "idle"); - ent.stopMoving(); - numToTake -= 1; - } - }); - } - } -}; - -EconomyManager.prototype.reassignIdleWorkers = function(gameState) { - - var self = this; - - // Search for idle workers, and tell them to gather resources based on demand - var filter = Filters.isIdle(); - var idleWorkers = gameState.updatingCollection("idle-workers", filter, gameState.getOwnEntitiesByRole("worker")); - - if (idleWorkers.length) { - idleWorkers.forEach(function(ent) { - // Check that the worker isn't garrisoned - if (ent.position() === undefined){ - return; - } - if (ent.hasClass("Worker")) { - var types = self.pickMostNeededResources(gameState); - ent.setMetadata(PlayerID, "subrole", "gatherer"); - ent.setMetadata(PlayerID, "gather-type", types[0]); - } else { - ent.setMetadata(PlayerID, "subrole", "hunter"); - } - - }); - } -}; - -EconomyManager.prototype.workersBySubrole = function(gameState, subrole) { - var workers = gameState.getOwnEntitiesByRole("worker"); - return gameState.updatingCollection("subrole-" + subrole, Filters.byMetadata(PlayerID, "subrole", subrole), workers); -}; - -EconomyManager.prototype.assignToFoundations = function(gameState, noRepair) { - // 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. - - var foundations = gameState.getOwnFoundations().toEntityArray(); - var damagedBuildings = gameState.getOwnEntities().filter(function (ent) { if (ent.needsRepair() && ent.getMetadata(PlayerID, "plan") == undefined) { return true; } return false; }).toEntityArray(); - - // Check if nothing to build - if (!foundations.length && !damagedBuildings.length){ - return; - } - var workers = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byClass("Cavalry"))); - var builderWorkers = this.workersBySubrole(gameState, "builder"); - - var addedWorkers = 0; - - var maxTotalBuilders = Math.ceil(this.numWorkers * 0.15); - - for (var i in foundations) { - var target = foundations[i]; - // Removed: sometimes the AI would not notice it has empty unbuilt fields - //if (target._template.BuildRestrictions.Category === "Field") - // continue; // we do not build fields - - var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length; - - var targetNB = this.targetNumBuilders; - if (target.hasClass("CivCentre") || target.buildTime() > 150 || target.hasClass("House")) - targetNB *= 2; - - if (assigned < targetNB) { - if (builderWorkers.length + addedWorkers < maxTotalBuilders) { - - var addedToThis = 0; - - var idleBuilders = builderWorkers.filter(Filters.isIdle()); - idleBuilders.forEach(function(ent) { - if (ent.position() && SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB) - { - addedWorkers++; - addedToThis++; - ent.setMetadata(PlayerID, "target-foundation", target); - } - }); - if (assigned + addedToThis < targetNB) - { - var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); - var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB - assigned - addedToThis); - nearestNonBuilders.forEach(function(ent) { - addedWorkers++; - addedToThis++; - ent.stopMoving(); - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target); - }); - } - } - } - } - // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed. - for (var i in damagedBuildings) { - var target = damagedBuildings[i]; - if (gameState.defcon() < 5) { - if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) { - continue; - } - } else if (noRepair && !target.hasClass("CivCentre")) - continue; - - var territory = Map.createTerritoryMap(gameState); - if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID) - continue; - - var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length; - if (assigned < this.targetNumBuilders/3) { - if (builderWorkers.length + addedWorkers < this.targetNumBuilders*2) { - - var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); - if (gameState.defcon() < 5) - nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); }); - var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders/3 - assigned); - - nearestNonBuilders.forEach(function(ent) { - ent.stopMoving(); - addedWorkers++; - ent.setMetadata(PlayerID, "subrole", "builder"); - ent.setMetadata(PlayerID, "target-foundation", target); - }); - } - } - } -}; - -EconomyManager.prototype.buildMoreFields = function(gameState, queues) { - if (this.farmingFields === true) { - var farms = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_field")); - var numFarms = 0; - farms.forEach(function (field) { - if (field.resourceSupplyAmount() > 400) - numFarms++; - }); - numFarms += gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field")) - numFarms += queues.field.countTotalQueuedUnits(); - - if (numFarms < this.targetNumFields) - queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field")); - } else { - var foodAmount = 0; - gameState.getOwnDropsites("food").forEach( function (ent) { //}){ - if (ent.getMetadata(PlayerID, "resource-quantity-food") != undefined) { - foodAmount += ent.getMetadata(PlayerID, "resource-quantity-food"); - } else { - foodAmount = 500; // wait till we initialize - } - }); - if (foodAmount < 500) - this.farmingFields = true; - } -}; - -// If all the CC's are destroyed then build a new one -EconomyManager.prototype.buildNewCC= function(gameState, queues) { - var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre")); - numCCs += queues.civilCentre.totalLength(); - - // no use trying to lay foundations that will be destroyed - if (gameState.defcon() > 2) - for ( var i = numCCs; i < 1; i++) { - gameState.ai.queueManager.clear(); - this.baseNeed["food"] = 0; - this.baseNeed["wood"] = 50; - this.baseNeed["stone"] = 50; - this.baseNeed["metal"] = 50; - queues.civilCentre.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre")); - } - return (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_civil_centre")) == 0 && gameState.currentPhase() > 1); -}; - -// TODO: make it regularly update stone+metal mines and their resource levels. -// creates and maintains a map of unused resource density -// this also takes dropsites into account. -// resources that are "part" of a dropsite are not counted. -EconomyManager.prototype.updateResourceMaps = function(gameState, events) { - - // By how much to divide the resource amount for plotting. - var decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; - // This is the maximum radius of the influence - var dpRadius = 10; - var radius = {'wood':10.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0}; - - // smallRadius is the distance necessary to mark a resource as linked to a dropsite. - var smallRadius = { 'food':90*90,'wood':55*55,'stone':70*70,'metal':70*70 }; - // bigRadius is the distance for a weak link (resources are considered when building other dropsites) - // and their resource amount is divided by 3 when checking for dropsite resource level. - var bigRadius = { 'food':100*100,'wood':100*100,'stone':140*140,'metal':140*140 }; - - var self = this; - - for (var resource in radius){ - // if there is no resourceMap create one with an influence for everything with that resource - if (! this.resourceMaps[resource]){ - // We're creting them 8-bit. Things could go above 255 if there are really tons of resources - // But at that point the precision is not really important anyway. And it saves memory. - this.resourceMaps[resource] = new Map(gameState, new Uint8Array(gameState.getMap().data.length)); - this.resourceMaps[resource].setMaxVal(255); - this.CCResourceMaps[resource] = new Map(gameState, new Uint8Array(gameState.getMap().data.length)); - this.CCResourceMaps[resource].setMaxVal(255); - } - } - - var needUpdate = {}; - - // Look for destroy events and subtract the entities original influence from the resourceMap - // also look for dropsite destruction and add the associated entities (along with unmarking them) - for (var key in events) { - var e = events[key]; - if (e.type === "Destroy") { - - if (e.msg.entityObj){ - var ent = e.msg.entityObj; - if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { - if (e.msg.metadata[PlayerID] && !e.msg.metadata[PlayerID]["linked-dropsite"]) { - var resource = ent.resourceSupplyType().generic; - var x = Math.round(ent.position()[0] / gameState.cellSize); - var z = Math.round(ent.position()[1] / gameState.cellSize); - var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); - - if (resource === "wood" || resource === "food") - { - this.resourceMaps[resource].addInfluence(x, z, 2, 5,'constant'); - this.resourceMaps[resource].addInfluence(x, z, 10.0, -strength,'constant'); - this.CCResourceMaps[resource].addInfluence(x, z, 15, -strength/2.0,'constant'); - } else if (resource === "stone" || resource === "metal") - { - this.resourceMaps[resource].addInfluence(x, z, 8, 50); - this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/1.5); - this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/2.0,'constant'); - this.CCResourceMaps[resource].addInfluence(x, z, 30, -strength,'constant'); - } - } - } - if (ent && ent.owner() == PlayerID && ent.resourceDropsiteTypes() !== undefined) { - var resources = ent.resourceDropsiteTypes(); - for (var i in resources) { - var resource = resources[i]; - // loop through all dropsites to see if the resources of his entity collection could - // be taken over by another dropsite - var dropsites = gameState.getOwnDropsites(resource); - var metadata = e.msg.metadata[PlayerID]; - - // can happen if it's destroyed before we've initialised it. - if (!metadata || !metadata["linked-resources-" + resource]) - break; - metadata["linked-resources-" + resource].filter( function (supply) { //}){ - var takenOver = false; - dropsites.forEach( function (otherDropsite) { //}) { - if (!otherDropsite.position() || otherDropsite.id() == ent.id()) - return; - var distance = SquareVectorDistance(supply.position(), otherDropsite.position()); - if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { - if (distance < bigRadius[resource]) { - supply.setMetadata(PlayerID, "linked-dropsite", otherDropsite.id() ); - supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance); - if (distance < smallRadius[resource]) { - takenOver = true; - supply.setMetadata(PlayerID, "linked-dropsite-nearby", true ); - } else { - supply.setMetadata(PlayerID, "linked-dropsite-nearby", false ); - } - } - } - }); - if (!takenOver) { - var x = Math.round(supply.position()[0] / gameState.cellSize); - var z = Math.round(supply.position()[1] / gameState.cellSize); - var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]); - if (resource === "wood" || resource === "food") - { - self.CCResourceMaps[resource].addInfluence(x, z, 15, strength/2.0,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 10.0, strength,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 2, -5,'constant'); - } else if (resource === "stone" || resource === "metal") - { - self.CCResourceMaps[resource].addInfluence(x, z, 30, strength,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 12.0, strength/1.5); - self.resourceMaps[resource].addInfluence(x, z, 12.0, strength/2.0,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 8, -50); - } - } - }); - needUpdate[resource] = true; - } - } - } - } else if (e.type === "Create") { - if (e.msg.entity){ - var ent = gameState.getEntityById(e.msg.entity); - if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure"){ - var resource = ent.resourceSupplyType().generic; - var addToMap = true; - var dropsites = gameState.getOwnDropsites(resource); - dropsites.forEach( function (otherDropsite) { //}) { - if (!otherDropsite.position()) - return; - var distance = SquareVectorDistance(ent.position(), otherDropsite.position()); - if (ent.getMetadata(PlayerID, "linked-dropsite") == undefined || ent.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { - if (distance < bigRadius[resource]) { - if (distance < smallRadius[resource]) { - if (ent.getMetadata(PlayerID, "linked-dropsite") == undefined) - addToMap = false; - ent.setMetadata(PlayerID, "linked-dropsite-nearby", true ); - } else { - ent.setMetadata(PlayerID, "linked-dropsite-nearby", false ); - } - ent.setMetadata(PlayerID, "linked-dropsite", otherDropsite.id() ); - ent.setMetadata(PlayerID, "linked-dropsite-dist", +distance); - } - } - }); - if (addToMap) { - var x = Math.round(ent.position()[0] / gameState.cellSize); - var z = Math.round(ent.position()[1] / gameState.cellSize); - var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); - if (resource === "wood" || resource === "food") - { - this.CCResourceMaps[resource].addInfluence(x, z, 15, strength/2.0,'constant'); - this.resourceMaps[resource].addInfluence(x, z, 10.0, strength,'constant'); - this.resourceMaps[resource].addInfluence(x, z, 2, -5,'constant'); - } else if (resource === "stone" || resource === "metal") - { - this.CCResourceMaps[resource].addInfluence(x, z, 30, strength,'constant'); - this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/1.5); - this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/2.0,'constant'); - this.resourceMaps[resource].addInfluence(x, z, 8, -50); - } - } - } else if (ent && ent.position() && ent.resourceDropsiteTypes) { - var resources = ent.resourceDropsiteTypes(); - for (var i in resources) { - var resource = resources[i]; - needUpdate[resource] = true; - } - } - } - } - } - - for (var i in needUpdate) - { - this.updateNearbyResources(gameState,i); - this.updateResourceConcentrations(gameState,i); - } - /* - if (gameState.ai.playedTurn % 20 === 1) - { - this.resourceMaps['wood'].dumpIm("s_tree_density_ " + gameState.getTimeElapsed() +".png", 255); - this.resourceMaps['stone'].dumpIm("stone_density_ " + gameState.getTimeElapsed() +".png", 255); - this.resourceMaps['metal'].dumpIm("s_metal_density_ " + gameState.getTimeElapsed() +".png", 255); - this.CCResourceMaps['wood'].dumpIm("CC_TREE " + gameState.getTimeElapsed() +".png", 255); - this.CCResourceMaps['stone'].dumpIm("CC_STONE " + gameState.getTimeElapsed() +".png", 255); - this.CCResourceMaps['metal'].dumpIm("CC_METAL " + gameState.getTimeElapsed() +".png", 255); - }*/ -}; - -// Returns the position of the best place to build a new dropsite for the specified resource -EconomyManager.prototype.getBestResourceBuildSpot = function(gameState, resource){ - - // 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. - - var friendlyTiles = new Map(gameState); - var territory = Map.createTerritoryMap(gameState); - - var obstructions = Map.createObstructionMap(gameState); - obstructions.expandInfluences(); - - var myDropsites = gameState.getOwnEntities().filter(Filters.isDropsite(resource)); - - for (var j = 0; j < friendlyTiles.length; ++j) - { - friendlyTiles.map[j] += this.resourceMaps[resource].map[j] * 1.5; - - // first pass: we remove anything not in our territory - // needed because the obstruction map is by default set true for BuildNeutral - if (territory.getOwnerIndex(j) !== PlayerID) - { - friendlyTiles.map[j] = 0; - continue; - } - // only add where the map is currently not null, ie in our territory and some "Resource" would be close. - // This makes the placement go from "OK" to "human-like". - for (var i in this.resourceMaps) - if (friendlyTiles.map[j] !== 0 && i !== "food") - friendlyTiles.map[j] += this.resourceMaps[i].map[j]; - - // mark as unbuildable if we're realy close from another dropsite. Might avoid rare bugs. - // TODO: should mostly examine why those happen, check top post page 4 of the "WIP new API" topic started by Wraitii. - for (var i in myDropsites._entities) - { - var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; - if (myDropsites._entities[i].position() && SquareVectorDistance(friendlyTiles.gamePosToMapPos(myDropsites._entities[i].position()), pos) < 100) - friendlyTiles.map[j] = 0; - } - } - - //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_dp_placement_base.png", 255); - - var isCivilCenter = false; - var best = friendlyTiles.findBestTile(2, obstructions); // try to find a spot to place a DP. - var bestIdx = best[0]; - - //debug ("Have " + best[1] + " for " + resource); - - // 75, from empirical values, seems reasonable. - if (best[1] <= 75 && gameState.currentPhase() >= 2) - { - // restart the search this time for a CC - friendlyTiles = new Map(gameState); - - var ents = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")); - var eEnts = gameState.getEnemyEntities().filter(Filters.byClass("CivCentre")); - - // This uses a different resource maps,where the point is basically to try to have as many resources as possible in the CC's territory. - for (var j = 0; j < friendlyTiles.length; ++j) - { - // We check for our other CCs: the distance must not be too big. Anything bigger will result in scrapping. - // This ensures territorial continuity. - // TODO: maybe whenever I get around to implement multi-base support (details below, requires being part of the team. If you're not, ask wraitii directly by PM). - // (see www.wildfiregames.com/forum/index.php?showtopic=16702&#entry255631 ) - var mindist = 7101; - var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; - ents.forEach( function (cc) { - var dist = SquareVectorDistance(friendlyTiles.gamePosToMapPos(cc.position()),pos); - if (dist < mindist) - mindist = dist; - }); - if (mindist > 7100) - { - friendlyTiles.map[j] = 0; - continue; - } - // Checking for enemy CCs - mindist = 7101; - eEnts.forEach( function (cc) { - var dist = SquareVectorDistance(friendlyTiles.gamePosToMapPos(cc.position()),pos); - if (dist < mindist) - mindist = dist; - }); - if (mindist < 3500) // cannot build too close to each other. - { - friendlyTiles.map[j] = 0; - continue; - } - - // mark as unbuildable if we're realy close from another dropsite. Might avoid rare bugs. - // TODO: should mostly examine why those happen, check top post page 4 of the "WIP new API" topic started by Wraitii. - for (var i in myDropsites._entities) - { - var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; - if (myDropsites._entities[i].position() && SquareVectorDistance(friendlyTiles.gamePosToMapPos(myDropsites._entities[i].position()), pos) < 100) - friendlyTiles.map[j] = 0; - } - - friendlyTiles.map[j] += this.CCResourceMaps[resource].map[j] * 1.5; - - for (var i in this.CCResourceMaps) - if (friendlyTiles.map[j] !== 0 && i !== "food") - friendlyTiles.map[j] += this.CCResourceMaps[i].map[j]; - } - - //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_cc_placement_base.png", 5000); - - best = friendlyTiles.findBestTile(4, obstructions); - bestIdx = best[0]; - isCivilCenter = true; - } else { - //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_final2.png", 5000); - } - - // tell the dropsite builder we haven't found anything satisfactory. - if (best[1] < 60) - return [false, [-1,0]]; - - var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; - var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; - - return [isCivilCenter, [x,z]]; -}; - -EconomyManager.prototype.updateResourceConcentrations = function(gameState, resource){ - var self = this; - gameState.getOwnDropsites(resource).forEach(function(dropsite) { //}){ - var amount = 0; - var amountFar = 0; - // loop through the entity collections of linked-resources, if there is one. - if (dropsite.getMetadata(PlayerID, "linked-resources-" + resource) == undefined) - return; - dropsite.getMetadata(PlayerID, "linked-resources-" + resource).forEach(function(supply){ //}){ - if (supply.isFull() === true || supply.getMetadata(PlayerID, "inaccessible") == true) - return; - - if (supply.getMetadata(PlayerID, "linked-dropsite-nearby") == true) - amount += supply.resourceSupplyAmount(); - else - amountFar += supply.resourceSupplyAmount(); - supply.setMetadata(PlayerID, "dp-update-value",supply.resourceSupplyAmount()); - }); - dropsite.setMetadata(PlayerID, "resource-quantity-" + resource, amount); - dropsite.setMetadata(PlayerID, "resource-quantity-far-" + resource, amountFar); - //debug (dropsite + " has " + amount + ", " + amountFar +" of " +resource); - }); -}; - -// Stores lists of nearby resources -// This is done only once per dropsite. -EconomyManager.prototype.updateNearbyResources = function(gameState,resource){ - var self = this; - var resources = ["food", "wood", "stone", "metal"]; - var resourceSupplies; - - // By how much to divide the resource amount for plotting. - var decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; - // This is the maximum radius of the influence - var radius = {'wood':10.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0}; - - // smallRadius is the distance necessary to mark a resource as linked to a dropsite. - var smallRadius = { 'food':90*90,'wood':55*55,'stone':70*70,'metal':70*70 }; - // bigRadius is the distance for a weak link (resources are considered when building other dropsites) - // and their resource amount is divided by 3 when checking for dropsite resource level. - var bigRadius = { 'food':100*100,'wood':100*100,'stone':140*140,'metal':140*140 }; - - gameState.getOwnDropsites(resource).forEach(function(ent) { //}){ - - if (ent.getMetadata(PlayerID, "nearby-resources-" + resource) === undefined){ - // let's defined the entity collections (by metadata) - gameState.getResourceSupplies(resource).filter( function (supply) { //}){ - if (!supply.position() || !ent.position()) - return; - var distance = SquareVectorDistance(supply.position(), ent.position()); - // if we're close than the current linked-dropsite, or if it's not linked - // TODO: change when actualy resource counting is implemented. - - if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { - if (distance < bigRadius[resource]) { - if (distance < smallRadius[resource]) { - // it's new to the game, remove it from the resource maps - if ((supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-nearby") == false) - && supply.resourceSupplyType().generic !== "treasure") { - var x = Math.round(supply.position()[0] / gameState.cellSize); - var z = Math.round(supply.position()[1] / gameState.cellSize); - var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]); - if (resource === "wood" || resource === "food") - { - self.CCResourceMaps[resource].addInfluence(x, z, 15, -strength/2.0,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 2, 5,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 10.0, -strength,'constant'); - } else if (resource === "stone" || resource === "metal") - { - self.CCResourceMaps[resource].addInfluence(x, z, 30, -strength,'constant'); - self.resourceMaps[resource].addInfluence(x, z, 8, 50); - self.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/1.5); - self.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/2.0,'constant'); - } - } - supply.setMetadata(PlayerID, "linked-dropsite-nearby", true ); - } else { - supply.setMetadata(PlayerID, "linked-dropsite-nearby", false ); - } - supply.setMetadata(PlayerID, "linked-dropsite", ent.id() ); - supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance); - } - } - }); - // This one is both for the nearby and the linked - var filter = Filters.byMetadata(PlayerID, "linked-dropsite", ent.id()); - var collection = gameState.getResourceSupplies(resource).filter(filter); - collection.registerUpdates(); - ent.setMetadata(PlayerID, "linked-resources-" + resource, collection); - - filter = Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true); - var collection2 = collection.filter(filter); - collection2.registerUpdates(); - ent.setMetadata(PlayerID, "nearby-resources-" + resource, collection2); - - } - - - - /*// Make resources glow wildly - if (resource == "food"){ - ent.getMetadata(PlayerID, "linked-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,0,0]}); - }); - ent.getMetadata(PlayerID, "nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); - }); - } - if (resource == "wood"){ - ent.getMetadata(PlayerID, "linked-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,1,0]}); - }); - ent.getMetadata(PlayerID, "nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); - }); - } - if (resource == "metal"){ - ent.getMetadata(PlayerID, "linked-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,1]}); - }); - ent.getMetadata(PlayerID, "nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]}); - }); - } - if (resource == "stone"){ - ent.getMetadata(PlayerID, "linked-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,1]}); - }); - ent.getMetadata(PlayerID, "nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,5,10]}); - }); - }*/ - }); -}; - -//return the number of resource dropsites with an acceptable amount of the resource nearby -EconomyManager.prototype.checkResourceConcentrations = function(gameState, resource){ - //TODO: make these values adaptive - var requiredInfluence = {"wood": 2500, "stone": 600, "metal": 600}; - var count = 0; - gameState.getOwnDropsites(resource).forEach(function(ent) { //}){ - if (ent.getMetadata(PlayerID, "resource-quantity-" + resource) == undefined || typeof(ent.getMetadata(PlayerID, "resource-quantity-" + resource)) !== "number") { - count++; // assume it's OK if we don't know. - return; - } - var quantity = +ent.getMetadata(PlayerID, "resource-quantity-" + resource); - var quantityFar = +ent.getMetadata(PlayerID, "resource-quantity-far-" + resource); - - if (quantity >= requiredInfluence[resource]) { - count++; - } else if (quantity + quantityFar >= requiredInfluence[resource]) { - count += 0.5 + (quantity/requiredInfluence[resource])/2; - } else { - count += ((quantity + quantityFar)/requiredInfluence[resource])/2; - } - }); - return count; -}; - -EconomyManager.prototype.buildTemple = function(gameState, queues){ - if (gameState.currentPhase() >= 2 ) { - if (queues.economicBuilding.countTotalQueuedUnits() === 0 && - gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple")) === 0){ - queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_temple")); - } - } -}; - -EconomyManager.prototype.buildMarket = function(gameState, queues){ - if (this.numWorkers > 50 && gameState.currentPhase() >= 2 ) { - if (queues.economicBuilding.countTotalQueuedUnitsWithClass("BarterMarket") === 0 && - gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){ - //only ever build one storehouse/CC/market at a time - queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_market")); - } - } -}; - -// Build a farmstead to go to town phase faster and prepare for research. Only really active on higher diff mode. -EconomyManager.prototype.buildFarmstead = function(gameState, queues){ - if (gameState.getTimeElapsed() > this.farmsteadStartTime) { - if (queues.economicBuilding.countTotalQueuedUnitsWithClass("DropsiteFood") === 0 && - gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead")) === 0){ - //only ever build one storehouse/CC/market at a time - queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_farmstead")); - } - } -}; - -EconomyManager.prototype.buildDock = function(gameState, queues){ - if (!gameState.ai.waterMap || this.dockFailed) - return; - if (gameState.getTimeElapsed() > this.dockStartTime) { - if (queues.economicBuilding.countTotalQueuedUnitsWithClass("NavalMarket") === 0 && - gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_dock")) === 0){ - if (gameState.civ() == "cart" && gameState.currentPhase() > 1) - queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_super_dock")); - else if (gameState.civ() !== "cart") - queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_dock")); - } - } -}; - -// if Aegis has resources it doesn't need, it'll try to barter it for resources it needs -// once per turn because the info doesn't update between a turn and I don't want to fix it. -// Not sure how efficient it is but it seems to be sane, at least. -EconomyManager.prototype.tryBartering = function(gameState){ - var done = false; - if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_market")) >= 1) { - - var needs = gameState.ai.queueManager.futureNeeds(gameState,false); - var ress = gameState.ai.queueManager.getAvailableResources(gameState); - - for (var sell in needs) { - for (var buy in needs) { - if (!done && buy != sell && needs[sell] <= 0 && ress[sell] > 400) { // if we don't need it and have a buffer - if ( (ress[buy] < 400) || needs[buy] > 0) { // if we need that other resource/ have too little of it - var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market")).toEntityArray(); - markets[0].barter(buy,sell,100); - //debug ("bartered " +sell +" for " + buy + ", value 100"); - done = true; - } - } - } - } - } -}; - -// TODO: while the algorithm for dropsite placement is quite good -// This is bad. Choosing when to place dropsites should be improved. -EconomyManager.prototype.buildDropsites = function(gameState, queues){ - if ( queues.dropsites.totalLength() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0 && - gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0){ - //only ever build one storehouse/CC/market at a time - if (gameState.getTimeElapsed() > 30 * 1000){ - var built = false; - for (var resource in this.dropsiteNumbers){ - if (this.checkResourceConcentrations(gameState, resource) < this.dropsiteNumbers[resource]){ - var spot = this.getBestResourceBuildSpot(gameState, resource); - if (spot[1][0] === -1) - break; - if (spot[0] === true){ - queues.dropsites.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot[1])); - } else { - queues.dropsites.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_storehouse", spot[1])); - } - built = true; - break; - } - } - if (!built) - for (var resource in this.dropsiteNumbers){ - if (this.checkResourceConcentrations(gameState, resource) < Math.ceil(this.dropsiteNumbers[resource])){ - var spot = this.getBestResourceBuildSpot(gameState, resource); - if (spot[1][0] === -1) - break; - if (spot[0] === true){ - queues.dropsites.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot[1])); - } else { - queues.dropsites.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_storehouse", spot[1])); - } - built = true; - break; - } - } - } - } -}; -// build more houses if needed. -// kinda ugly, lots of special cases to both build enough houses but not tooo many… -EconomyManager.prototype.buildMoreHouses = function(gameState, queues) { - if ( (this.fastStart && gameState.getTimeElapsed() < 10000) || gameState.getTimeElapsed() < 35000) - return; - - // TODO: temporary 'remaining population space' based check, need to do - // predictive in future - if (gameState.getPopulationLimit() - gameState.getPopulation() < 20 - && gameState.getPopulationLimit() < gameState.getPopulationMax()) { - - var numConstructing = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house")); - var numPlanned = queues.house.totalLength(); - - var additional = 0; - - if (gameState.currentPhase() > 1) - { - if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") - additional = Math.ceil((30 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 5) - numConstructing - numPlanned; - else - additional = Math.ceil((30 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 10) - numConstructing - numPlanned; - } else { - if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") - additional = Math.ceil((20 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 5) - numConstructing - numPlanned; - else - additional = Math.ceil((20 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 10) - numConstructing - numPlanned; - } - if (Config.difficulty === 3) - additional *= 2; // we don't build enough otherwise. - - for ( var i = 0; i < additional; i++) { - queues.house.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_house")); - } - } -}; - -// Change our priorities based on our gathering statistics. -// TODO: this is currently unused, I'm not sure how sensible it is -// This should probably be scrapped in favor of improving the queueManager's detection -// of future needs. -EconomyManager.prototype.rePrioritize = function(gameState) { - var statG = gameState.playerData.statistics.resourcesGathered; - var statU = gameState.playerData.statistics.resourcesUsed; - var resources = ["food", "wood", "stone", "metal"]; - for each (var ress in resources) - { - var eff = (statG[ress]-this.lastStatG[ress]) / (statU[ress]-this.lastStatU[ress]); - - if ((statU[ress]-this.lastStatU[ress]) === 0) - continue; - - if (eff < 0.6) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] + 10); - else if (eff < 0.7) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] + 6); - else if (eff < 0.8) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] + 4); - else if (eff > 1.2) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] - 10 ); - else if (eff > 1.1) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] - 8 ); - else if (eff > 1.0) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] - 5 ); - else if (eff > 0.9) - this.baseNeed[ress] = Math.max(10, this.baseNeed[ress] - 3 ); - //debug (ress + " Eff: " + eff); - } - - //debug ("Stats "); - //debug ("Food: " + this.baseNeed["food"]); - //debug ("Wood: " + this.baseNeed["wood"]); - //debug ("Stone: " + this.baseNeed["stone"]); - //debug ("Metal: " + this.baseNeed["metal"]); - - this.lastStatG = statG; - this.lastStatU = statU; -}; - -EconomyManager.prototype.update = function(gameState, queues, events) { - Engine.ProfileStart("economy update"); - - this.reassignRolelessUnits(gameState); - - // run a particular BO if we have no CC as this is highest priority - if (this.buildNewCC(gameState,queues)) - { - Engine.ProfileStart("Update Resource Maps and Concentrations"); - this.updateResourceMaps(gameState, events); - - if (gameState.ai.playedTurn % 2 === 0) { - var resources = ["food", "wood", "stone", "metal"]; - this.updateNearbyResources(gameState, resources[(gameState.ai.playedTurn % 8)/2]); - } else if (gameState.ai.playedTurn % 2 === 1) { - var resources = ["food", "wood", "stone", "metal"]; - this.updateResourceConcentrations(gameState, resources[((gameState.ai.playedTurn+1) % 8)/2]); - } - Engine.ProfileStop(); - - if (Config.difficulty !== 0) - this.tryBartering(gameState); - - if (gameState.ai.playedTurn % 20 === 0){ - this.setWorkersIdleByPriority(gameState); - } else { - Engine.ProfileStart("Reassign Idle Workers"); - this.reassignIdleWorkers(gameState); - Engine.ProfileStop(); - - Engine.ProfileStart("Assign builders"); - this.assignToFoundations(gameState, false); - Engine.ProfileStop(); - } - - Engine.ProfileStart("Run Workers"); - gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ - if (!ent.getMetadata(PlayerID, "worker-object")){ - ent.setMetadata(PlayerID, "worker-object", new Worker(ent)); - } - ent.getMetadata(PlayerID, "worker-object").update(gameState); - }); - - Engine.ProfileStop(); - Engine.ProfileStop(); - return; - } - - // Normal run - - this.numWorkers = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID,"plan"))).length; - - // this function also deals with a few things that are number-of-workers related - Engine.ProfileStart("Train workers and build farms, houses. Research techs."); - this.trainMoreWorkers(gameState, queues); - - if ((gameState.ai.playedTurn+2) % 20 === 0 && gameState.getTimeElapsed() > this.techStartTime) - this.tryResearchTechs(gameState,queues); - - if ((gameState.ai.playedTurn+1) % 3 === 0) - this.buildMoreFields(gameState,queues); - - this.buildMoreHouses(gameState,queues); - - Engine.ProfileStop(); - - //Later in the game we want to build stuff faster. - if (gameState.getTimeElapsed() > 15*60*1000) { - this.targetNumBuilders = Config.Economy.targetNumBuilders*4; - }else if (gameState.getTimeElapsed() > 5*60*1000) { - this.targetNumBuilders = Config.Economy.targetNumBuilders*2; - } else { - this.targetNumBuilders = Config.Economy.targetNumBuilders; - } - if (gameState.currentPhase() == 1 && !this.fastStart) - this.femaleRatio = Config.Economy.femaleRatio * 1.3; - else - this.femaleRatio = Config.Economy.femaleRatio; - - if (gameState.getTimeElapsed() > 600000 && this.numWorkers < 50 && Config.difficulty > 0) - { - gameState.ai.queueManager.changePriority("villager", 80); - gameState.ai.queueManager.changePriority("citizenSoldier", 70); - } else if (gameState.getTimeElapsed() > 600000 && this.numWorkers > 80 && Config.difficulty > 0 - && gameState.ai.queueManager.priorities["villager"] == 80) - { - gameState.ai.queueManager.changePriority("villager", Config.priorities.villager); - gameState.ai.queueManager.changePriority("citizenSoldier", Config.priorities.citizenSoldier); - } - - if (this.baseNeed["food"] === 300 && (this.numWorkers >= 15 || gameState.isResearching("phase_town_generic") || gameState.isResearching("phase_town_athens"))) { - this.baseNeed["food"] -= 150; - } else if (this.baseNeed["metal"] === 0 && (gameState.currentPhase() === 2 || gameState.isResearching("phase_town_generic") || gameState.isResearching("phase_town_athens"))) { - // for the little while in town phase, we want a little more more stone/wood than usual - this.baseNeed["food"] = 100; - this.baseNeed["wood"] = 100; - this.baseNeed["stone"] = 80; - this.baseNeed["metal"] = 50; - if (gameState.civ() == "maur" || gameState.civ() == "brit" || gameState.civ() == "gaul") - { - this.baseNeed["wood"] = 120; - this.baseNeed["stone"] = 60; - } - } else if (this.baseNeed["stone"] === 80 && (gameState.currentPhase() === 3 || gameState.isResearching("phase_city_generic")) ) - { - // switch back to less stone but push metal. - this.baseNeed["food"] = 80; - this.baseNeed["wood"] = 80; - this.baseNeed["stone"] = 45; - this.baseNeed["metal"] = 65; - } - //if (Config.difficulty === 2 && gameState.getTimeElapsed() > 900000 && gameState.ai.playedTurn % 60 === 10) - // this.rePrioritize(gameState); - - Engine.ProfileStart("Update Resource Maps and Concentrations"); - this.updateResourceMaps(gameState, events); - if (gameState.ai.playedTurn % 2 === 0) { - var resources = ["food", "wood", "stone", "metal"]; - this.updateNearbyResources(gameState, resources[(gameState.ai.playedTurn % 8)/2]); - } else if (gameState.ai.playedTurn % 2 === 1) { - var resources = ["food", "wood", "stone", "metal"]; - this.updateResourceConcentrations(gameState, resources[((gameState.ai.playedTurn+1) % 8)/2]); - } - Engine.ProfileStop(); - - if (gameState.ai.playedTurn % 4 === 0) { - Engine.ProfileStart("Build new Dropsites"); - this.buildDropsites(gameState, queues); - Engine.ProfileStop(); - } - if (Config.difficulty !== 0) - this.tryBartering(gameState); - - this.buildFarmstead(gameState, queues); - this.buildMarket(gameState, queues); - // Deactivated: the temple had no useful purpose for the AI now. - //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 1) - // this.buildTemple(gameState, queues); - this.buildDock(gameState, queues); // not if not a water map. - - if (gameState.ai.playedTurn % 10 === 0){ - this.setWorkersIdleByPriority(gameState); - } - if (gameState.ai.playedTurn % 3 === 1) - { - Engine.ProfileStart("Reassign Idle Workers"); - this.reassignIdleWorkers(gameState); - Engine.ProfileStop(); - } - - // this is pretty slow, run it once in a while - if (gameState.ai.playedTurn % 6 === 1) { - Engine.ProfileStart("Swap Workers"); - var gathererGroups = {}; - gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ - if (ent.hasClass("Cavalry")) - return; - var key = uneval(ent.resourceGatherRates()); - if (!gathererGroups[key]){ - gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; - } - if (ent.getMetadata(PlayerID, "gather-type") in gathererGroups[key]){ - gathererGroups[key][ent.getMetadata(PlayerID, "gather-type")].push(ent); - } - }); - for (var i in gathererGroups){ - for (var j in gathererGroups){ - var a = eval(i); - var b = eval(j); - if (a !== undefined && b !== undefined) - if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 - && gathererGroups[j]["food"].length > 0){ - for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ - gathererGroups[i]["wood"][k].setMetadata(PlayerID, "gather-type", "food"); - gathererGroups[j]["food"][k].setMetadata(PlayerID, "gather-type", "wood"); - } - } - } - } - Engine.ProfileStop(); - } - - Engine.ProfileStart("Assign builders"); - this.assignToFoundations(gameState); - Engine.ProfileStop(); - - // TODO: do this incrementally a la defence.js (Changed slightly to be faster already). - Engine.ProfileStart("Run Workers"); - gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ - if (!ent.getMetadata(PlayerID, "worker-object")) - ent.setMetadata(PlayerID, "worker-object", new Worker(ent)); - if ((ent.id() + gameState.ai.playedTurn) % 3 === 0) // should make it significantly faster without much drawbacks. - ent.getMetadata(PlayerID, "worker-object").update(gameState); - }); - - Engine.ProfileStop(); - Engine.ProfileStop(); -}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js b/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js index 5d82e6926a..0256e48433 100755 --- a/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js +++ b/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js @@ -12,7 +12,10 @@ var enemyWatcher = function(gameState, playerToWatch) { var filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(this.watched)); this.enemyBuildings = gameState.updatingGlobalCollection("player-" +this.watched + "-structures", filter); - + + filter = Filters.and(Filters.byClass("Unit"), Filters.byOwner(this.watched)); + this.enemyUnits = gameState.updatingGlobalCollection("player-" +this.watched + "-units", filter); + filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched)); this.enemyCivilians = gameState.updatingGlobalCollection("player-" +this.watched + "-civilians", filter); diff --git a/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js b/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js index 43352247c4..9aff32f42c 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js +++ b/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js @@ -6,5 +6,5 @@ function EntityCollectionFromIds(gameState, idList){ ents[id] = gameState.entities._entities[id]; } } - return new EntityCollection(gameState.ai, ents); + return new EntityCollection(gameState.sharedScript, ents); } diff --git a/binaries/data/mods/public/simulation/ai/aegis/headquarters.js b/binaries/data/mods/public/simulation/ai/aegis/headquarters.js new file mode 100644 index 0000000000..3a02fd097e --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/aegis/headquarters.js @@ -0,0 +1,1236 @@ +/* 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) + > researching + -picking strategy (specific manager?) + -diplomacy (specific manager?) + -planning attacks + -picking new CC locations. + */ + +var HQ = function() { + this.targetNumBuilders = Config.Economy.targetNumBuilders; // number of workers we want building stuff + + this.dockStartTime = Config.Economy.dockStartTime * 1000; + this.techStartTime = Config.Economy.techStartTime * 1000; + + this.dockFailed = false; // sanity check + this.waterMap = false; // set by the aegis.js file. + + // tell if we can't gather from a resource type for sanity checks. + this.outOf = { "food" : false, "wood" : false, "stone" : false, "metal" : false }; + + this.baseManagers = {}; + + // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. + this.femaleRatio = Config.Economy.femaleRatio; + + this.fortressStartTime = 0; + this.fortressLapseTime = Config.Military.fortressLapseTime * 1000; + this.defenceBuildingTime = Config.Military.defenceBuildingTime * 1000; + this.attackPlansStartTime = Config.Military.attackPlansStartTime * 1000; + this.defenceManager = new Defence(); + + this.navalManager = new NavalManager(); + + this.TotalAttackNumber = 0; + this.upcomingAttacks = { "CityAttack" : [] }; + this.startedAttacks = { "CityAttack" : [] }; +}; + +// More initialisation for stuff that needs the gameState +HQ.prototype.init = function(gameState, events, queues){ + // initialize base map. Each pixel is a base ID, or 0 if none + this.basesMap = new Map(gameState.sharedScript, new Uint8Array(gameState.getMap().data.length)); + this.basesMap.setMaxVal(255); + + if (Config.Economy.targetNumWorkers) + this.targetNumWorkers = Config.Economy.targetNumWorkers; + else if (this.targetNumWorkers === undefined) + this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*(0.2 + Math.min(+(Config.difficulty)*0.125,0.3))), 1); + + + // Let's get our initial situation here. + // TODO: improve on this. + // TODO: aknowledge bases, assign workers already. + var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID)); + + var workersNB = 0; + var hasScout = false; + var treasureAmount = { 'food': 0, 'wood': 0, 'stone': 0, 'metal': 0 }; + var hasCC = false; + + if (ents.filter(Filters.byClass("CivCentre")).length > 0) + hasCC = true; + workersNB = ents.filter(Filters.byClass("Worker")).length; + if (ents.filter(Filters.byClass("Cavalry")).length > 0) + hasScout = true; + + // tODO: take multiple CCs into account. + if (hasCC) + { + var CC = ents.filter(Filters.byClass("CivCentre")).toEntityArray()[0]; + for (i in treasureAmount) + gameState.getResourceSupplies(i).forEach( function (ent) { + if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), CC.position()) < 5000) + treasureAmount[i] += ent.resourceSupplyMax(); + }); + this.baseManagers[1] = new BaseManager(); + this.baseManagers[1].init(gameState, events); + this.baseManagers[1].setAnchor(CC); + this.baseManagers[1].initTerritory(this, gameState); + this.baseManagers[1].initGatheringFunctions(this, gameState); + + if (Config.debug) + this.basesMap.dumpIm("basesMap.png"); + var self = this; + + ents.forEach( function (ent) { //}){ + self.baseManagers[1].assignEntity(ent); + }); + } + // we now have enough data to decide on a few things. + + // TODO: here would be where we pick our initial strategy. + + // immediatly build a wood dropsite if possible. + if (this.baseManagers[1]) + { + if (gameState.ai.queueManager.getAvailableResources(gameState)["wood"] >= 250) + { + var pos = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); + if (pos) + { + queues.dropsites.addItem(new ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : 1 }, 0, -1, pos)); + queues.minorTech.addItem(new ResearchPlan(gameState, "gather_capacity_wheelbarrow")); + } + } + } + + var map = new Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps["wood"].map); + if (Config.debug) + map.dumpIm("map_CC_Wood.png"); + + //this.reassignIdleWorkers(gameState); + + + this.navalManager.init(gameState, events, queues); + + // TODO: change that. + var civ = gameState.playerData.civ; + + // load units and buildings from the config files + + if (civ in Config.buildings.moderate){ + this.bModerate = Config.buildings.moderate[civ]; + }else{ + this.bModerate = Config.buildings.moderate['default']; + } + + if (civ in Config.buildings.advanced){ + this.bAdvanced = Config.buildings.advanced[civ]; + }else{ + this.bAdvanced = Config.buildings.advanced['default']; + } + + if (civ in Config.buildings.fort){ + this.bFort = Config.buildings.fort[civ]; + }else{ + this.bFort = Config.buildings.fort['default']; + } + + for (var i in this.bAdvanced){ + this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); + } + for (var i in this.bFort){ + this.bFort[i] = gameState.applyCiv(this.bFort[i]); + } + + // TODO: figure out how to make this generic + for (var i in this.attackManagers){ + this.availableAttacks[i] = new this.attackManagers[i](gameState, this); + } + + var enemies = gameState.getEnemyEntities(); + var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); + this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes + this.enemySoldiers.registerUpdates(); + + // each enemy watchers keeps a list of entity collections about the enemy it watches + // It also keeps track of enemy armies, merging/splitting as needed + // TODO: remove those. + this.enemyWatchers = {}; + this.ennWatcherIndex = []; + for (var i = 1; i <= 8; i++) + if (PlayerID != i && gameState.isPlayerEnemy(i)) { + this.enemyWatchers[i] = new enemyWatcher(gameState, i); + this.ennWatcherIndex.push(i); + this.defenceManager.enemyArmy[i] = []; + } +}; + +HQ.prototype.checkEvents = function (gameState, events, queues) { + for (i in events) + { + if (events[i].type == "Destroy") + { + // TODO: probably check stuffs like a base destruction. + } else if (events[i].type == "Create") + { + var evt = events[i]; + // Let's check if we have a building set to create a new base. + if (evt.msg && evt.msg.entity) + { + var ent = gameState.getEntityById(evt.msg.entity); + if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "base") === -1) + { + // Okay so let's try to create a new base around this. + var bID = uniqueIDBases; + this.baseManagers[bID] = new BaseManager(); + this.baseManagers[bID].init(gameState, events, true); + this.baseManagers[bID].setAnchor(ent); + this.baseManagers[bID].initTerritory(this, gameState); + + // Let's get a few units out there to build this. + // TODO: select the best base, or use multiple bases. + var builders = this.bulkPickWorkers(gameState, bID, 10); + builders.forEach(function (worker) { + worker.setMetadata(PlayerID, "base", bID); + worker.setMetadata(PlayerID, "subrole", "builder"); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); + } + } + } else if (events[i].type == "ConstructionFinished") + { + var evt = events[i]; + // Let's check if we have a building set to create a new base. + // TODO: move to the base manager. + if (evt.msg && evt.msg.newentity) + { + var ent = gameState.getEntityById(evt.msg.newentity); + if (ent && ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "baseAnchor") == true) + { + var base = ent.getMetadata(PlayerID, "base"); + if (this.baseManagers[base].constructing) + { + this.baseManagers[base].constructing = false; + this.baseManagers[base].initGatheringFunctions(this, gameState); + } + } + } + } + } +}; + +// okay, so here we'll create both females and male workers. +// We'll try to keep close to the "ratio" defined atop. +// Choice of citizen soldier is a bit messy. +// Before having 100 workers it focuses on speed, cost, and won't choose units that cost stone/metal +// After 100 it just picks the strongest; +// TODO: This should probably be changed to favor a more mixed approach for better defense. +// (or even to adapt based on estimated enemy strategy). +// TODO: this should probably set which base it wants them in. +HQ.prototype.trainMoreWorkers = function(gameState, queues) { + // Count the workers in the world and in progress + var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen")); + numFemales += queues.villager.countQueuedUnitsWithClass("Support"); + + // counting the workers that aren't part of a plan + var numWorkers = 0; + gameState.getOwnEntities().forEach (function (ent) { + if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") == undefined) + numWorkers++; + }); + var numInTraining = 0; + gameState.getOwnTrainingFacilities().forEach(function(ent) { + ent.trainingQueue().forEach(function(item) { + if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan == undefined) + numWorkers += item.count; + numInTraining += item.count; + }); + }); + var numQueued = queues.villager.countQueuedUnits() + queues.citizenSoldier.countQueuedUnits(); + var numTotal = numWorkers + numQueued; + + // If we have too few, train more + // should plan enough to always have females… + // TODO: 15 here should be changed to something more sensible, such as nb of producing buildings. + if (numTotal < this.targetNumWorkers && numQueued < 50 && (queues.villager.length() + queues.citizenSoldier.length()) < 120 && numInTraining < 15) { + var template = gameState.applyCiv("units/{civ}_support_female_citizen"); + + var size = Math.min(5, Math.ceil(numTotal / 10)); + + if (numFemales/numTotal > this.femaleRatio && (numTotal > 20 || (this.fastStart && numTotal > 10))) { + if (numTotal < 100) + template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]); + else + template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["strength",1] ]); + if (!template) + template = gameState.applyCiv("units/{civ}_support_female_citizen"); + if (gameState.currentPhase() === 1) + size = 2; + } + + if (numFemales/numTotal > this.femaleRatio * 1.3) + queues.villager.paused = true; + else if ((numFemales/numTotal < this.femaleRatio * 1.1) || gameState.ai.queueManager.getAvailableResources(gameState)["food"] > 250) + queues.villager.paused = false; + + // TODO: perhaps assign them a default resource and check the base according to that. + + // base "0" means "auto" + if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) + queues.villager.addItem(new TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size )); + else + queues.citizenSoldier.addItem(new TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size)); + } +}; + +// picks the best template based on parameters and classes +HQ.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { + var units = gameState.findTrainableUnits(classes); + + if (units.length === 0) + return undefined; + + units.sort(function(a, b) {// }) { + var aDivParam = 0, bDivParam = 0; + var aTopParam = 0, bTopParam = 0; + for (var i in parameters) { + var param = parameters[i]; + + if (param[0] == "base") { + aTopParam = param[1]; + bTopParam = param[1]; + } + if (param[0] == "strength") { + aTopParam += getMaxStrength(a[1]) * param[1]; + bTopParam += getMaxStrength(b[1]) * param[1]; + } + if (param[0] == "speed") { + aTopParam += a[1].walkSpeed() * param[1]; + bTopParam += b[1].walkSpeed() * param[1]; + } + + if (param[0] == "cost") { + aDivParam += a[1].costSum() * param[1]; + bDivParam += b[1].costSum() * param[1]; + } + // requires a third parameter which is the resource + if (param[0] == "costsResource") { + if (a[1].cost()[param[2]]) + aTopParam *= param[1]; + if (b[1].cost()[param[2]]) + bTopParam *= param[1]; + } + } + return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); + }); + return units[0][0]; +}; + +// picks the best template based on parameters and classes +HQ.prototype.findBestTrainableSoldier = function(gameState, classes, parameters) { + var units = gameState.findTrainableUnits(classes); + + if (units.length === 0) + return undefined; + + + units.sort(function(a, b) { //}) { + var aDivParam = 0, bDivParam = 0; + var aTopParam = 0, bTopParam = 0; + for (var i in parameters) { + var param = parameters[i]; + + if (param[0] == "base") { + aTopParam = param[1]; + bTopParam = param[1]; + } + if (param[0] == "strength") { + aTopParam += getMaxStrength(a[1]) * param[1]; + bTopParam += getMaxStrength(b[1]) * param[1]; + } + if (param[0] == "siegeStrength") { + aTopParam += getMaxStrength(a[1], "Structure") * param[1]; + bTopParam += getMaxStrength(b[1], "Structure") * param[1]; + } + if (param[0] == "speed") { + aTopParam += a[1].walkSpeed() * param[1]; + bTopParam += b[1].walkSpeed() * param[1]; + } + + if (param[0] == "cost") { + aDivParam += a[1].costSum() * param[1]; + bDivParam += b[1].costSum() * param[1]; + } + if (param[0] == "canGather") { + // checking against wood, could be anything else really. + if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) + aTopParam *= param[1]; + if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) + bTopParam *= param[1]; + } + } + return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); + }); + return units[0][0]; +}; + + +// Tries to research any available tech +// Only one at once. Also does military tech (selection is completely random atm) +// TODO: Lots, lots, lots here. +HQ.prototype.tryResearchTechs = function(gameState, queues) { + if (queues.minorTech.length() === 0) + { + var possibilities = gameState.findAvailableTech(); + if (possibilities.length === 0) + return; + // randomly pick one. No worries about pairs in that case. + var p = Math.floor((Math.random()*possibilities.length)); + queues.minorTech.addItem(new ResearchPlan(gameState, possibilities[p][0])); + } +} + +// We're given a worker and a resource type +// We'll assign the worker for the best base for that resource type. +// TODO: improve choice alogrithm +HQ.prototype.switchWorkerBase = function(gameState, worker, type) { + var bestBase = 0; + for (var i in this.baseManagers) + { + if (this.baseManagers[i].willGather[type] >= 1) + { + if (this.baseManagers[i].accessIndex === this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex + || this.navalManager.canReach(gameState, this.baseManagers[i].accessIndex, this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex)) + { + bestBase = i; + break; + } + } + } + if (bestBase && bestBase !== worker.getMetadata(PlayerID,"base")) + { + worker.setMetadata(PlayerID,"base",bestBase); + return true; + } else { + return false; + } +}; + +// returns an entity collection of workers through BaseManager.pickBuilders +// TODO: better the choice algo. +// TODO: also can't get over multiple bases right now. +HQ.prototype.bulkPickWorkers = function(gameState, newBaseID, number) { + var accessIndex = this.baseManagers[newBaseID].accessIndex; + if (!accessIndex) + return false; + // sorting bases by whether they are on the same accessindex or not. + var baseBest = AssocArraytoArray(this.baseManagers).sort(function (a,b) { + if (a.accessIndex === accessIndex && b.accessIndex !== accessIndex) + return -1; + else if (b.accessIndex === accessIndex && a.accessIndex !== accessIndex) + return 1; + return 0; + }); + for (i in baseBest) + { + if (baseBest[i].workers.length > number) + { + return baseBest[i].pickBuilders(gameState,number); + } + } + return false; +} + +// 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. +HQ.prototype.GetCurrentGatherRates = function(gameState) { + var self = this; + + var currentRates = {}; + for (var type in this.wantedRates) + currentRates[type] = 0; + + for (i in this.baseManagers) + this.baseManagers[i].getGatherRates(gameState, currentRates); + + return currentRates; +}; + + +// Pick the resource which most needs another worker +HQ.prototype.pickMostNeededResources = function(gameState) { + var self = this; + + this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); + + var currentRates = {}; + for (var type in this.wantedRates) + currentRates[type] = 0; + + for (i in this.baseManagers) + { + var base = this.baseManagers[i]; + for (var type in this.wantedRates) + { + if (gameState.turnCache["gathererAssignementCache-" + type]) + currentRates[type] += gameState.turnCache["gathererAssignementCache-" + type]; + + base.gatherersByType(gameState,type).forEach (function (ent) { //}){ + var worker = ent.getMetadata(PlayerID, "worker-object"); + if (worker) + currentRates[type] += worker.getGatherRate(gameState); + }); + } + } + + // let's get our ideal number. + + var types = Object.keys(this.wantedRates); + + types.sort(function(a, b) { + var va = (Math.max(0,self.wantedRates[a] - currentRates[a]))/ (currentRates[a]+1); + var vb = (Math.max(0,self.wantedRates[b] - currentRates[b]))/ (currentRates[b]+1); + + // If they happen to be equal (generally this means "0" aka no need), make it equitable. + if (va === vb) + return (self.wantedRates[b]/(currentRates[b]+1)) - (self.wantedRates[a]/(currentRates[a]+1)); + return vb-va; + }); + return types; +}; + +// If all the CC's are destroyed then build a new one +// TODO: rehabilitate. +HQ.prototype.buildNewCC= function(gameState, queues) { + var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre")); + numCCs += queues.civilCentre.length(); + + // no use trying to lay foundations that will be destroyed + if (gameState.defcon() > 2) + for ( var i = numCCs; i < 1; i++) { + gameState.ai.queueManager.clear(); + this.baseNeed["food"] = 0; + this.baseNeed["wood"] = 50; + this.baseNeed["stone"] = 50; + this.baseNeed["metal"] = 50; + queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre")); + } + return (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) == 0 && gameState.currentPhase() > 1); +}; + +// Returns the best position to build a new Civil Centre +// Whose primary function would be to reach new resources of type "resource". +HQ.prototype.findBestEcoCCLocation = function(gameState, resource){ + + var CCPlate = gameState.getTemplate("structures/{civ}_civil_centre"); + + // 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. + + var territory = Map.createTerritoryMap(gameState); + + var obstructions = Map.createObstructionMap(gameState, 0); + obstructions.expandInfluences(); + + // copy the resource map as initialization. + var friendlyTiles = new Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps[resource].map, true); + friendlyTiles.setMaxVal(255); + var ents = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")).toEntityArray(); + var eEnts = gameState.getEnemyEntities().filter(Filters.byClass("CivCentre")).toEntityArray(); + + var dps = gameState.getOwnDropsites().toEntityArray(); + + for (var j = 0; j < friendlyTiles.length; ++j) + { + // We check for our other CCs: the distance must not be too big. Anything bigger will result in scrapping. + // This ensures territorial continuity. + // TODO: maybe whenever I get around to implement multi-base support (details below, requires being part of the team. If you're not, ask wraitii directly by PM). + // (see www.wildfiregames.com/forum/index.php?showtopic=16702&#entry255631 ) + // TODO: figure out what I was trying to say above. + + var canBuild = true; + var canBuild2 = false; + for (var i in ents) + { + var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; + var dist = SquareVectorDistance(friendlyTiles.gamePosToMapPos(ents[i].position()), pos); + if (dist < 2025) + { + canBuild = false; + continue; + } else if (dist < 8000 || this.waterMap) + canBuild2 = true; + } + // checking for bases. + if (this.basesMap.map[j] !== 0) + canBuild = false; + + if (!canBuild2) + canBuild = false; + if (canBuild) + { + // Checking for enemy CCs + for (var i in eEnts) + { + var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; + // 7100 works well as a limit. + if (SquareVectorDistance(friendlyTiles.gamePosToMapPos(eEnts[i].position()), pos) < 2500) + { + canBuild = false; + continue; + } + } + } + if (!canBuild) + { + friendlyTiles.map[j] = 0; + continue; + } + + for (var i in dps) + { + var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; + var dpPos = dps[i].position(); + if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 100) + { + friendlyTiles.map[j] = 0; + continue; + } else if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 400) + friendlyTiles.map[j] /= 2; + } + + friendlyTiles.map[j] *= 1.5; + + for (var i in gameState.sharedScript.CCResourceMaps) + if (friendlyTiles.map[j] !== 0 && i !== "food") + { + var val = friendlyTiles.map[j] + gameState.sharedScript.CCResourceMaps[i].map[j]; + if (val < 255) + friendlyTiles.map[j] = val; + else + friendlyTiles.map[j] = 255; + } + } + + + var best = friendlyTiles.findBestTile(3, obstructions); + var bestIdx = best[0]; + + if (Config.debug) + { + friendlyTiles.map[bestIdx] = 255; + friendlyTiles.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + ".png", 5000); + obstructions.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + "_obs.png", 20); + } + debug ("Best at " + best[1]); + + // not good enough. + if (best[1] < 60) + return false; + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + return [x,z]; +}; + +HQ.prototype.buildTemple = function(gameState, queues){ + if (gameState.currentPhase() >= 2 ) { + if (queues.economicBuilding.countQueuedUnits() === 0 && + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple")) === 0){ + queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_temple", { "base" : 1 })); + } + } +}; + +HQ.prototype.buildMarket = function(gameState, queues){ + if (gameState.getPopulation() > Config.Economy.popForMarket && gameState.currentPhase() >= 2 ) { + if (queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") === 0 && + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){ + //only ever build one storehouse/CC/market at a time + queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_market", { "base" : 1 })); + } + } +}; + +// Build a farmstead to go to town phase faster and prepare for research. Only really active on higher diff mode. +HQ.prototype.buildFarmstead = function(gameState, queues){ + if (gameState.getPopulation() > Config.Economy.popForFarmstead) { + if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") === 0 && + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead")) === 0){ + //only ever build one storehouse/CC/market at a time + queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_farmstead", { "base" : 1 })); + } + } +}; + +// TODO: generic this, probably per-base +HQ.prototype.buildDock = function(gameState, queues){ + if (!this.waterMap || this.dockFailed) + return; + if (gameState.getTimeElapsed() > this.dockStartTime) { + if (queues.economicBuilding.countQueuedUnitsWithClass("NavalMarket") === 0 && + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_dock")) === 0) { + var tp = "" + if (gameState.civ() == "cart" && gameState.currentPhase() > 1) + tp = "structures/{civ}_super_dock"; + else if (gameState.civ() !== "cart") + tp = "structures/{civ}_dock"; + if (tp !== "") + { + var remaining = this.navalManager.getUnconnectedSeas(gameState, this.baseManagers[1].accessIndex); + queues.economicBuilding.addItem(new ConstructionPlan(gameState, tp, { "base" : 1, "sea" : remaining[0] })); + } + } + } +}; + +// if Aegis has resources it doesn't need, it'll try to barter it for resources it needs +// once per turn because the info doesn't update between a turn and I don't want to fix it. +// Not sure how efficient it is but it seems to be sane, at least. +HQ.prototype.tryBartering = function(gameState){ + var done = false; + if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true) >= 1) { + + var needs = gameState.ai.queueManager.futureNeeds(gameState); + var ress = gameState.ai.queueManager.getAvailableResources(gameState); + + for (var sell in needs) { + for (var buy in needs) { + if (!done && buy != sell && needs[sell] <= 0 && ress[sell] > 400) { // if we don't need it and have a buffer + if ( (ress[buy] < 400) || needs[buy] > 0) { // if we need that other resource/ have too little of it + var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true).toEntityArray(); + markets[0].barter(buy,sell,100); + //debug ("bartered " +sell +" for " + buy + ", value 100"); + done = true; + } + } + } + } + } +}; + +// build more houses if needed. +// kinda ugly, lots of special cases to both build enough houses but not tooo many… +HQ.prototype.buildMoreHouses = function(gameState,queues) { + + if (gameState.getPopulationLimit() < gameState.getPopulationMax()) { + + var numPlanned = queues.house.length(); + + if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) + { + var plan = new ConstructionPlan(gameState, "structures/{civ}_house", { "base" : 1 }); + // change the starting condition to "less than 15 slots left". + plan.isGo = function (gameState) { + var HouseNb = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true); + + var freeSlots = 0; + if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") + freeSlots = gameState.getPopulationLimit() + HouseNb*5 - gameState.getPopulation(); + else + freeSlots = gameState.getPopulationLimit() + HouseNb*10 - gameState.getPopulation(); + if (gameState.getPopulation() > 55 && Config.difficulty > 1) + return (freeSlots <= 21); + else if (gameState.getPopulation() >= 20 && Config.difficulty !== 0) + return (freeSlots <= 16); + else + return (freeSlots <= 10); + } + queues.house.addItem(plan); + } + } +}; + +// checks if we have bases for all resource types (bar food for now) or if we need to expand. +HQ.prototype.checkBasesRessLevel = function(gameState,queues) { + if (gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) + return; + var count = { "wood" : 0, "stone" : 0, "metal" : 0 } + var capacity = { "wood" : 0, "stone" : 0, "metal" : 0 } + var need = { "wood" : true, "stone" : true, "metal" : true }; + var posss = []; + for (i in this.baseManagers) + { + var base = this.baseManagers[i]; + for (type in count) + { + if (base.getResourceLevel(gameState, type, "all") > 1500*Math.max(Config.difficulty,2)) + count[type]++; + capacity[type] += base.getWorkerCapacity(gameState, type); + if (base.willGather[type] !== 2) + need[type] = false; + } + } + for (type in count) + { + if (count[type] === 0 || need[type] + || capacity[type] < gameState.getOwnEntities().filter(Filters.and(Filters.byMetadata(PlayerID, "subrole", "gatherer"), Filters.byMetadata(PlayerID, "gather-type", type))).length * 1.05) + { + // plan a new base. + if (gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0 && queues.civilCentre.length() === 0) { + if (this.outOf[type] && gameState.ai.playedTurn % 10 !== 0) + continue; + var pos = this.findBestEcoCCLocation(gameState, type); + if (!pos) + { + // Okay so we'll set us as out of this. + this.outOf[type] = true; + } else { + // base "-1" means new base. + queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre",{ "base" : -1 }, 0, -1, pos)); + } + } + } + } +}; + +// Deals with building fortresses and towers. +// Currently build towers next to every useful dropsites. +// TODO: Fortresses are placed randomly atm. +HQ.prototype.buildDefences = function(gameState, queues){ + + var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID,"plan"))).length; + + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower')) + + queues.defenceBuilding.length() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.length() < 4 && gameState.currentPhase() > 1) { + for (i in this.baseManagers) + { + for (j in this.baseManagers[i].dropsites) + { + var amnts = this.baseManagers[i].dropsites[j]; + var dpEnt = gameState.getEntityById(j); + if (dpEnt !== undefined && dpEnt.getMetadata(PlayerID, "defenseTower") !== true) + if (amnts["wood"] || amnts["metal"] || amnts["stone"]) + { + var position = dpEnt.position(); + if (position) { + queues.defenceBuilding.addItem(new ConstructionPlan(gameState, 'structures/{civ}_defense_tower', { "base" : i }, 0 , -1, position)); + } + dpEnt.setMetadata(PlayerID, "defenseTower", true); + } + } + } + } + + var numFortresses = 0; + for (var i in this.bFort){ + numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i])); + } + + if (queues.defenceBuilding.length() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) + { + if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) + { + if (!this.fortressStartTime) + this.fortressStartTime = gameState.getTimeElapsed(); + queues.defenceBuilding.addItem(new ConstructionPlan(gameState, this.bFort[0], { "base" : 1 })); + debug ("Building a fortress"); + } + } + if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i]), true) >= 1) { + // let's add a siege building plan to the current attack plan if there is none currently. + if (this.upcomingAttacks["CityAttack"].length !== 0) + { + var attack = this.upcomingAttacks["CityAttack"][0]; + if (!attack.unitStat["Siege"]) + { + // no minsize as we don't want the plan to fail at the last minute though. + var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], + "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; + if (gameState.civ() == "cart" || gameState.civ() == "maur") + stat["classes"] = ["Elephant"]; + attack.addBuildOrder(gameState, "Siege", stat, true); + } + } + } +}; + +HQ.prototype.buildBlacksmith = function(gameState, queues){ + if (gameState.getTimeElapsed() > Config.Military.timeForBlacksmith*1000) { + if (queues.militaryBuilding.length() === 0 && + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_blacksmith")) === 0) { + var tp = gameState.getTemplate(gameState.applyCiv("structures/{civ}_blacksmith")); + if (tp.available(gameState)) + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "base" : 1 })); + } + } +}; + +// Deals with constructing military buildings (barracks, stables…) +// They are mostly defined by Config.js. This is unreliable since changes could be done easily. +// TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. +// TODO: building placement is bad. Choice of buildings is also fairly dumb. +HQ.prototype.constructTrainingBuildings = function(gameState, queues) { + Engine.ProfileStart("Build buildings"); + var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID, "plan"))).length; + + if (workersNumber > Config.Military.popForBarracks1) { + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.length() < 1) { + debug ("Trying to build barracks"); + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + } + } + + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 2 && workersNumber > Config.Military.popForBarracks2) + if (queues.militaryBuilding.length() < 1) + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + + if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) === 2 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 3 && workersNumber > 125) + if (queues.militaryBuilding.length() < 1) + { + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + } + } + //build advanced military buildings + if (workersNumber >= Config.Military.popForBarracks2 - 15 && gameState.currentPhase() > 2){ + if (queues.militaryBuilding.length() === 0){ + var inConst = 0; + for (var i in this.bAdvanced) + inConst += gameState.countFoundationsWithType(gameState.applyCiv(this.bAdvanced[i])); + if (inConst == 0 && this.bAdvanced && this.bAdvanced.length !== 0) { + var i = Math.floor(Math.random() * this.bAdvanced.length); + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + } + } + } + } + if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && + workersNumber > 130 && gameState.currentPhase() > 2) + { + var Const = 0; + for (var i in this.bAdvanced) + Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i]), true); + if (inConst == 1) { + var i = Math.floor(Math.random() * this.bAdvanced.length); + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + } + } + } + + Engine.ProfileStop(); +}; + +// TODO: use pop(). Currently unused as this is too gameable. +HQ.prototype.garrisonAllFemales = function(gameState) { + var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); + var females = gameState.getOwnEntities().filter(Filters.byClass("Support")); + + var cache = {}; + + females.forEach( function (ent) { + for (var i in buildings) + { + if (ent.position()) + { + var struct = buildings[i]; + if (!cache[struct.id()]) + cache[struct.id()] = 0; + if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) + { + ent.garrison(struct); + cache[struct.id()]++; + break; + } + } + } + }); + this.hasGarrisonedFemales = true; +}; +HQ.prototype.ungarrisonAll = function(gameState) { + this.hasGarrisonedFemales = false; + var buildings = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Structure"),Filters.byCanGarrison())).toEntityArray(); + buildings.forEach( function (struct) { + if (struct.garrisoned() && struct.garrisoned().length) + struct.unloadAll(); + }); +}; + +HQ.prototype.pausePlan = function(gameState, planName) { + for (var attackType in this.upcomingAttacks) { + for (var i in this.upcomingAttacks[attackType]) { + var attack = this.upcomingAttacks[attackType][i]; + if (attack.getName() == planName) + attack.setPaused(gameState, true); + } + } + for (var attackType in this.startedAttacks) { + for (var i in this.startedAttacks[attackType]) { + var attack = this.startedAttacks[attackType][i]; + if (attack.getName() == planName) + attack.setPaused(gameState, true); + } + } +} +HQ.prototype.unpausePlan = function(gameState, planName) { + for (var attackType in this.upcomingAttacks) { + for (var i in this.upcomingAttacks[attackType]) { + var attack = this.upcomingAttacks[attackType][i]; + if (attack.getName() == planName) + attack.setPaused(gameState, false); + } + } + for (var attackType in this.startedAttacks) { + for (var i in this.startedAttacks[attackType]) { + var attack = this.startedAttacks[attackType][i]; + if (attack.getName() == planName) + attack.setPaused(gameState, false); + } + } +} +HQ.prototype.pauseAllPlans = function(gameState) { + for (var attackType in this.upcomingAttacks) { + for (var i in this.upcomingAttacks[attackType]) { + var attack = this.upcomingAttacks[attackType][i]; + attack.setPaused(gameState, true); + } + } + for (var attackType in this.startedAttacks) { + for (var i in this.startedAttacks[attackType]) { + var attack = this.startedAttacks[attackType][i]; + attack.setPaused(gameState, true); + } + } +} +HQ.prototype.unpauseAllPlans = function(gameState) { + for (var attackType in this.upcomingAttacks) { + for (var i in this.upcomingAttacks[attackType]) { + var attack = this.upcomingAttacks[attackType][i]; + attack.setPaused(gameState, false); + } + } + for (var attackType in this.startedAttacks) { + for (var i in this.startedAttacks[attackType]) { + var attack = this.startedAttacks[attackType][i]; + attack.setPaused(gameState, false); + } + } +} + + +// Some functions are run every turn +// Others once in a while +HQ.prototype.update = function(gameState, queues, events) { + Engine.ProfileStart("Headquarters update"); + + this.checkEvents(gameState,events,queues); + //this.buildMoreHouses(gameState); + + //Engine.ProfileStart("Train workers and build farms, houses. Research techs."); + this.trainMoreWorkers(gameState, queues); + + // sandbox doesn't expand. + if (Config.difficulty !== 0) + this.checkBasesRessLevel(gameState, queues); + + this.buildMoreHouses(gameState,queues); + + if (gameState.getTimeElapsed() > this.techStartTime && gameState.currentPhase() > 2) + this.tryResearchTechs(gameState,queues); + + if (Config.difficulty > 1) + this.tryBartering(gameState); + + this.buildFarmstead(gameState, queues); + this.buildMarket(gameState, queues); + // Deactivated: the temple had no useful purpose for the AI now. + //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 1) + // this.buildTemple(gameState, queues); + this.buildDock(gameState, queues); // not if not a water map. + + Engine.ProfileStart("Constructing military buildings and building defences"); + this.constructTrainingBuildings(gameState, queues); + + this.buildBlacksmith(gameState, queues); + + if(gameState.getTimeElapsed() > this.defenceBuildingTime) + this.buildDefences(gameState, queues); + Engine.ProfileStop(); + + for (i in this.baseManagers) + { + this.baseManagers[i].checkEvents(gameState, events, queues) + if ( ( (+i + gameState.ai.playedTurn) % (uniqueIDBases - 1)) === 0) + this.baseManagers[i].update(gameState, queues, events); + } + + this.navalManager.update(gameState, queues, events); + + this.defenceManager.update(gameState, events, this); + + Engine.ProfileStart("Looping through attack plans"); + + // TODO: bump this into a function. + // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. + if (1) { + for (var attackType in this.upcomingAttacks) { + for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { + + var attack = this.upcomingAttacks[attackType][i]; + + // okay so we'll get the support plan + if (!attack.isStarted()) { + var updateStep = attack.updatePreparation(gameState, this,events); + + // now we're gonna check if the preparation time is over + if (updateStep === 1 || attack.isPaused() ) { + // just chillin' + } else if (updateStep === 0 || updateStep === 3) { + debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); + if (updateStep === 3) { + this.attackPlansEncounteredWater = true; + debug("No attack path found. Aborting."); + } + attack.Abort(gameState, this); + this.upcomingAttacks[attackType].splice(i--,1); + } else if (updateStep === 2) { + var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + if (Math.random() < 0.2) + chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + else if (Math.random() < 0.3) + chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + else if (Math.random() < 0.3) + chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + gameState.ai.chatTeam(chatText); + + debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + attack.StartAttack(gameState,this); + this.startedAttacks[attackType].push(attack); + this.upcomingAttacks[attackType].splice(i--,1); + } + } else { + var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + if (Math.random() < 0.2) + chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + else if (Math.random() < 0.3) + chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + else if (Math.random() < 0.3) + chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; + gameState.ai.chatTeam(chatText); + + debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + this.startedAttacks[attackType].push(attack); + this.upcomingAttacks[attackType].splice(i--,1); + } + } + } + } + for (var attackType in this.startedAttacks) { + for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { + var attack = this.startedAttacks[attackType][i]; + // okay so then we'll update the attack. + if (!attack.isPaused()) + { + var remaining = attack.update(gameState,this,events); + if (remaining == 0 || remaining == undefined) { + debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); + attack.Abort(gameState); + this.startedAttacks[attackType].splice(i--,1); + } + } + } + } + + // TODO: remove the limitation to attacks when on water maps. + + // Note: these indications of "rush" are currently unused. + if (gameState.ai.strategy === "rush" && this.startedAttacks["CityAttack"].length !== 0) { + // and then we revert. + gameState.ai.strategy = "normal"; + Config.Economy.femaleRatio = 0.4; + gameState.ai.modules.economy.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1); + } else if (gameState.ai.strategy === "rush" && this.upcomingAttacks["CityAttack"].length === 0) + { + Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "rush") + this.TotalAttackNumber++; + this.upcomingAttacks["CityAttack"].push(Lalala); + debug ("Starting a little something"); + } else if (gameState.ai.strategy !== "rush" && !this.waterMap) + { + // creating plans after updating because an aborted plan might be reused in that case. + if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && !this.attackPlansEncounteredWater + && gameState.getTimeElapsed() > this.attackPlansStartTime && gameState.currentPhase() > 1) { + if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && this.waterMap) + { + // wait till we get a dock. + } else { + // basically only the first plan, really. + if (this.upcomingAttacks["CityAttack"].length == 0 && gameState.getTimeElapsed() < 12*60000) { + var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1); + if (Lalala.failed) + { + this.attackPlansEncounteredWater = true; // hack + } else { + debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); + this.TotalAttackNumber++; + this.upcomingAttacks["CityAttack"].push(Lalala); + } + } else if (this.upcomingAttacks["CityAttack"].length == 0 && Config.difficulty !== 0) { + var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized"); + if (Lalala.failed) + { + this.attackPlansEncounteredWater = true; // hack + } else { + debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); + this.TotalAttackNumber++; + this.upcomingAttacks["CityAttack"].push(Lalala); + } + } + } + } + } + + /* + // very old relic. This should be reimplemented someday so the code stays here. + + if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { + var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); + if (!Lalala.createSupportPlans(gameState, this, )) { + debug ("Military Manager: harrassing plan not a valid option"); + this.HarassRaiding = false; + } else { + debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); + + this.totalStartedAttackNumber++; + this.preparingRaidNumber++; + this.currentAttacks.push(Lalala); + } + } + */ + Engine.ProfileStop(); + + /* + Engine.ProfileStop(); + + Engine.ProfileStart("Build new Dropsites"); + this.buildDropsites(gameState, queues); + Engine.ProfileStop(); + + if (Config.difficulty !== 0) + this.tryBartering(gameState); + + this.buildFarmstead(gameState, queues); + this.buildMarket(gameState, queues); + // Deactivated: the temple had no useful purpose for the AI now. + //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 1) + // this.buildTemple(gameState, queues); + this.buildDock(gameState, queues); // not if not a water map. +*/ + Engine.ProfileStop(); // Heaquarters update +}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/map-module.js b/binaries/data/mods/public/simulation/ai/aegis/map-module.js index 635e6547e8..9cd8070ffd 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/map-module.js +++ b/binaries/data/mods/public/simulation/ai/aegis/map-module.js @@ -1,44 +1,8 @@ -const TERRITORY_PLAYER_MASK = 0x3F; +// other map functions -//TODO: Make this cope with negative cell values -// This is by default a 16-bit map but can be adapted into 8-bit. -function Map(gameState, originalMap, actualCopy){ - // get the map to find out the correct dimensions - var gameMap = gameState.getMap(); - this.width = gameMap.width; - this.height = gameMap.height; - this.length = gameMap.data.length; - - this.maxVal = 65535; - - if (originalMap && actualCopy){ - this.map = new Uint16Array(this.length); - for (var i = 0; i < originalMap.length; ++i) - this.map[i] = originalMap[i]; - } else if (originalMap) { - this.map = originalMap; - } else { - this.map = new Uint16Array(this.length); - } - this.cellSize = gameState.cellSize; -} -Map.prototype.setMaxVal = function(val){ - this.maxVal = val; -}; - -Map.prototype.gamePosToMapPos = function(p){ - return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)]; -}; - -Map.prototype.point = function(p){ - var q = this.gamePosToMapPos(p); - return this.map[q[0] + this.width * q[1]]; -}; - -// returns an 8-bit map. -Map.createObstructionMap = function(gameState, template){ +Map.createObstructionMap = function(gameState, accessIndex, template){ var passabilityMap = gameState.getMap(); - var territoryMap = gameState.ai.territoryMap; + var territoryMap = gameState.ai.territoryMap; // default values var placementType = "land"; @@ -49,12 +13,12 @@ Map.createObstructionMap = function(gameState, template){ // 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"); + buildOwn = template.hasBuildTerritory("own"); + buildAlly = template.hasBuildTerritory("ally"); + buildNeutral = template.hasBuildTerritory("neutral"); buildEnemy = template.hasBuildTerritory("enemy"); } - + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-land"); if (placementType == "shore") @@ -66,10 +30,26 @@ Map.createObstructionMap = function(gameState, template){ { for (var y = 0; y < passabilityMap.height; ++y) { - okay = false; var i = x + y*passabilityMap.width; var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK); + + if (gameState.ai.myIndex !== gameState.ai.accessibility.landPassMap[i]) + { + obstructionTiles[i] = 0; + continue; + } + if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0) + { + obstructionTiles[i] = 0; + continue; + } + if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | gameState.getPassabilityClassMask("default")))) + { + obstructionTiles[i] = 0; + continue; + } + okay = false; var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]]; var available = 0; for each (var stuff in positions) @@ -78,10 +58,11 @@ Map.createObstructionMap = function(gameState, template){ var index2 = x + stuff[0]*2 + (y+stuff[1]*2)*passabilityMap.width; var index3 = x + stuff[0]*3 + (y+stuff[1]*3)*passabilityMap.width; var index4 = x + stuff[0]*4 + (y+stuff[1]*4)*passabilityMap.width; - if ((passabilityMap.data[index] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index) > 500) - if ((passabilityMap.data[index2] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index2) > 500) - if ((passabilityMap.data[index3] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index3) > 500) - if ((passabilityMap.data[index4] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index4) > 500) { + + if ((passabilityMap.data[index] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index,true) > 500) + if ((passabilityMap.data[index2] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index2,true) > 500) + if ((passabilityMap.data[index3] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index3,true) > 500) + if ((passabilityMap.data[index4] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index4,true) > 500) { if (available < 2) available++; else @@ -98,12 +79,6 @@ Map.createObstructionMap = function(gameState, template){ if (gameState.ai.terrainAnalyzer.map[id] === 0 || gameState.ai.terrainAnalyzer.map[id] == 30 || gameState.ai.terrainAnalyzer.map[id] == 40) okay = false; } - if (gameState.ai.myIndex !== gameState.ai.accessibility.passMap[i]) - okay = false; - if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0) - okay = false; - if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | gameState.getPassabilityClassMask("default")))) - okay = false; obstructionTiles[i] = okay ? 255 : 0; } } @@ -120,14 +95,17 @@ Map.createObstructionMap = function(gameState, template){ (!buildNeutral && tilePlayer == 0) || (!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) ); - var tileAccessible = (gameState.ai.myIndex === gameState.ai.accessibility.passMap[i]); + if (accessIndex) + var tileAccessible = (accessIndex === gameState.ai.accessibility.landPassMap[i]); + else + var tileAccessible = true; if (placementType === "shore") tileAccessible = true; obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 255; } } - - var map = new Map(gameState, obstructionTiles); + + var map = new Map(gameState.sharedScript, obstructionTiles); map.setMaxVal(255); if (template && template.buildDistance()){ @@ -135,23 +113,24 @@ Map.createObstructionMap = function(gameState, template){ var category = template.buildDistance().FromCategory; if (minDist !== undefined && category !== undefined){ gameState.getOwnEntities().forEach(function(ent) { - if (ent.buildCategory() === category && ent.position()){ - var pos = ent.position(); - var x = Math.round(pos[0] / gameState.cellSize); - var z = Math.round(pos[1] / gameState.cellSize); - map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant'); - } - }); + if (ent.buildCategory() === category && ent.position()){ + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant'); + } + }); } } - return map; }; + + Map.createTerritoryMap = function(gameState) { var map = gameState.ai.territoryMap; - var ret = new Map(gameState, map.data); + var ret = new Map(gameState.sharedScript, map.data); ret.getOwner = function(p) { return this.point(p) & TERRITORY_PLAYER_MASK; @@ -161,218 +140,3 @@ Map.createTerritoryMap = function(gameState) { } return ret; }; - -Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { - strength = strength ? +strength : +maxDist; - type = type ? type : 'linear'; - - var x0 = Math.max(0, cx - maxDist); - var y0 = Math.max(0, cy - maxDist); - var x1 = Math.min(this.width, cx + maxDist); - var y1 = Math.min(this.height, cy + maxDist); - var maxDist2 = maxDist * maxDist; - - var str = 0.0; - switch (type){ - case 'linear': - str = +strength / +maxDist; - break; - case 'quadratic': - str = +strength / +maxDist2; - break; - case 'constant': - str = +strength; - break; - } - - for ( var y = y0; y < y1; ++y) { - for ( var x = x0; x < x1; ++x) { - var dx = x - cx; - var dy = y - cy; - var r2 = dx*dx + dy*dy; - if (r2 < maxDist2){ - var quant = 0; - switch (type){ - case 'linear': - var r = Math.sqrt(r2); - quant = str * (maxDist - r); - break; - case 'quadratic': - quant = str * (maxDist2 - r2); - break; - case 'constant': - quant = str; - break; - } - if (this.map[x + y * this.width] + quant < 0) - this.map[x + y * this.width] = 0; - else if (this.map[x + y * this.width] + quant > this.maxVal) - this.map[x + y * this.width] = this.maxVal; // avoids overflow. - else - this.map[x + y * this.width] += quant; - } - } - } -}; - -Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) { - strength = strength ? +strength : +maxDist; - type = type ? type : 'constant'; - - var x0 = Math.max(0, cx - maxDist); - var y0 = Math.max(0, cy - maxDist); - var x1 = Math.min(this.width, cx + maxDist); - var y1 = Math.min(this.height, cy + maxDist); - var maxDist2 = maxDist * maxDist; - - var str = 0.0; - switch (type){ - case 'linear': - str = strength / maxDist; - break; - case 'quadratic': - str = strength / maxDist2; - break; - case 'constant': - str = strength; - break; - } - - for ( var y = y0; y < y1; ++y) { - for ( var x = x0; x < x1; ++x) { - var dx = x - cx; - var dy = y - cy; - var r2 = dx*dx + dy*dy; - if (r2 < maxDist2){ - var quant = 0; - switch (type){ - case 'linear': - var r = Math.sqrt(r2); - quant = str * (maxDist - r); - break; - case 'quadratic': - quant = str * (maxDist2 - r2); - break; - case 'constant': - quant = str; - break; - } - var machin = this.map[x + y * this.width] * quant; - if (machin < 0) - this.map[x + y * this.width] = 0; - else if (machin > this.maxVal) - this.map[x + y * this.width] = this.maxVal; - else - this.map[x + y * this.width] = machin; - } - } - } -}; -// doesn't check for overflow. -Map.prototype.setInfluence = function(cx, cy, maxDist, value) { - value = value ? value : 0; - - var x0 = Math.max(0, cx - maxDist); - var y0 = Math.max(0, cy - maxDist); - var x1 = Math.min(this.width, cx + maxDist); - var y1 = Math.min(this.height, cy + maxDist); - var maxDist2 = maxDist * maxDist; - - for ( var y = y0; y < y1; ++y) { - for ( var x = x0; x < x1; ++x) { - var dx = x - cx; - var dy = y - cy; - var r2 = dx*dx + dy*dy; - if (r2 < maxDist2){ - this.map[x + y * this.width] = value; - } - } - } -}; - -/** - * Make each cell's 16-bit/8-bit value at least one greater than each of its - * neighbours' values. (If the grid is initialised with 0s and 65535s or 255s, the - * result of each cell is its Manhattan distance to the nearest 0.) - */ -Map.prototype.expandInfluences = function() { - var w = this.width; - var h = this.height; - var grid = this.map; - for ( var y = 0; y < h; ++y) { - var min = this.maxVal; - for ( var x = 0; x < w; ++x) { - var g = grid[x + y * w]; - if (g > min) - grid[x + y * w] = min; - else if (g < min) - min = g; - ++min; - } - - for ( var x = w - 2; x >= 0; --x) { - var g = grid[x + y * w]; - if (g > min) - grid[x + y * w] = min; - else if (g < min) - min = g; - ++min; - } - } - - for ( var x = 0; x < w; ++x) { - var min = this.maxVal; - for ( var y = 0; y < h; ++y) { - var g = grid[x + y * w]; - if (g > min) - grid[x + y * w] = min; - else if (g < min) - min = g; - ++min; - } - - for ( var y = h - 2; y >= 0; --y) { - var g = grid[x + y * w]; - if (g > min) - grid[x + y * w] = min; - else if (g < min) - min = g; - ++min; - } - } -}; - -Map.prototype.findBestTile = function(radius, obstructionTiles){ - // Find the best non-obstructed tile - var bestIdx = 0; - var bestVal = -1; - for ( var i = 0; i < this.length; ++i) { - if (obstructionTiles.map[i] > radius) { - var v = this.map[i]; - if (v > bestVal) { - bestVal = v; - bestIdx = i; - } - } - } - - return [bestIdx, bestVal]; -}; - -// add to current map by the parameter map pixelwise -Map.prototype.add = function(map){ - for (var i = 0; i < this.length; ++i) { - if (this.map[i] + map.map[i] < 0) - this.map[i] = 0; - else if (this.map[i] + map.map[i] > this.maxVal) - this.map[i] = this.maxVal; - else - this.map[i] += map.map[i]; - } -}; - -Map.prototype.dumpIm = function(name, threshold){ - name = name ? name : "default.png"; - threshold = threshold ? threshold : this.maxVal; - Engine.DumpImage(name, this.map, this.width, this.height, threshold); -}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/military.js b/binaries/data/mods/public/simulation/ai/aegis/military.js deleted file mode 100755 index cb93a3b2af..0000000000 --- a/binaries/data/mods/public/simulation/ai/aegis/military.js +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Military Manager. - * Basically this deals with constructing defense and attack buildings, but it's not very developped yet. - * There's a lot of work still to do here. - * It also handles the attack plans (see attack_plan.js) - * Not completely cleaned up from the original version in qBot. - */ - -var MilitaryAttackManager = function() { - - this.fortressStartTime = 0; - this.fortressLapseTime = Config.Military.fortressLapseTime * 1000; - this.defenceBuildingTime = Config.Military.defenceBuildingTime * 1000; - this.attackPlansStartTime = Config.Military.attackPlansStartTime * 1000; - this.defenceManager = new Defence(); - - - this.TotalAttackNumber = 0; - this.upcomingAttacks = { "CityAttack" : [] }; - this.startedAttacks = { "CityAttack" : [] }; -}; - -MilitaryAttackManager.prototype.init = function(gameState) { - var civ = gameState.playerData.civ; - - // load units and buildings from the config files - - if (civ in Config.buildings.moderate){ - this.bModerate = Config.buildings.moderate[civ]; - }else{ - this.bModerate = Config.buildings.moderate['default']; - } - - if (civ in Config.buildings.advanced){ - this.bAdvanced = Config.buildings.advanced[civ]; - }else{ - this.bAdvanced = Config.buildings.advanced['default']; - } - - if (civ in Config.buildings.fort){ - this.bFort = Config.buildings.fort[civ]; - }else{ - this.bFort = Config.buildings.fort['default']; - } - - for (var i in this.bAdvanced){ - this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); - } - for (var i in this.bFort){ - this.bFort[i] = gameState.applyCiv(this.bFort[i]); - } - - // TODO: figure out how to make this generic - for (var i in this.attackManagers){ - this.availableAttacks[i] = new this.attackManagers[i](gameState, this); - } - - var enemies = gameState.getEnemyEntities(); - var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); - this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes - this.enemySoldiers.registerUpdates(); - - // each enemy watchers keeps a list of entity collections about the enemy it watches - // It also keeps track of enemy armies, merging/splitting as needed - this.enemyWatchers = {}; - this.ennWatcherIndex = []; - for (var i = 1; i <= 8; i++) - if (PlayerID != i && gameState.isPlayerEnemy(i)) { - this.enemyWatchers[i] = new enemyWatcher(gameState, i); - this.ennWatcherIndex.push(i); - this.defenceManager.enemyArmy[i] = []; - } - -}; - -// picks the best template based on parameters and classes -MilitaryAttackManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { - var units = gameState.findTrainableUnits(classes); - - if (units.length === 0) - return undefined; - - - units.sort(function(a, b) { //}) { - var aDivParam = 0, bDivParam = 0; - var aTopParam = 0, bTopParam = 0; - for (var i in parameters) { - var param = parameters[i]; - - if (param[0] == "base") { - aTopParam = param[1]; - bTopParam = param[1]; - } - if (param[0] == "strength") { - aTopParam += getMaxStrength(a[1]) * param[1]; - bTopParam += getMaxStrength(b[1]) * param[1]; - } - if (param[0] == "siegeStrength") { - aTopParam += getMaxStrength(a[1], "Structure") * param[1]; - bTopParam += getMaxStrength(b[1], "Structure") * param[1]; - } - if (param[0] == "speed") { - aTopParam += a[1].walkSpeed() * param[1]; - bTopParam += b[1].walkSpeed() * param[1]; - } - - if (param[0] == "cost") { - aDivParam += a[1].costSum() * param[1]; - bDivParam += b[1].costSum() * param[1]; - } - if (param[0] == "canGather") { - // checking against wood, could be anything else really. - if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) - aTopParam *= param[1]; - if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) - bTopParam *= param[1]; - } - } - return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); - }); - return units[0][0]; -}; - -// Deals with building fortresses and towers. -// Currently build towers next to every useful dropsites. -// TODO: Fortresses are placed randomly atm. -MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){ - - var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID,"plan"))).length; - - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower')) - + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.totalLength() < 4 - && gameState.currentPhase() > 1 && queues.defenceBuilding.totalLength() < 3) { - gameState.getOwnEntities().forEach(function(dropsiteEnt) { - if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata(PlayerID, "defenseTower") !== true - && (dropsiteEnt.getMetadata(PlayerID, "resource-quantity-wood") > 400 || dropsiteEnt.getMetadata(PlayerID, "resource-quantity-stone") > 500 - || dropsiteEnt.getMetadata(PlayerID, "resource-quantity-metal") > 500) ){ - var position = dropsiteEnt.position(); - if (position){ - queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position)); - } - dropsiteEnt.setMetadata(PlayerID, "defenseTower", true); - } - }); - } - - var numFortresses = 0; - for (var i in this.bFort){ - numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i])); - } - - if (queues.defenceBuilding.totalLength() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) - { - if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) - { - if (!this.fortressStartTime) - this.fortressStartTime = gameState.getTimeElapsed(); - queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0])); - debug ("Building a fortress"); - } - } - if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i])) >= 1) { - // let's add a siege building plan to the current attack plan if there is none currently. - if (this.upcomingAttacks["CityAttack"].length !== 0) - { - var attack = this.upcomingAttacks["CityAttack"][0]; - if (!attack.unitStat["Siege"]) - { - // no minsize as we don't want the plan to fail at the last minute though. - var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], - "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; - if (gameState.civ() == "cart" || gameState.civ() == "maur") - stat["classes"] = ["Elephant"]; - attack.addBuildOrder(gameState, "Siege", stat, true); - } - } - } -}; - -// Deals with constructing military buildings (barracks, stables…) -// They are mostly defined by Config.js. This is unreliable since changes could be done easily. -// TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. -// TODO: building placement is bad. Choice of buildings is also fairly dumb. -MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) { - Engine.ProfileStart("Build buildings"); - var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID, "plan"))).length; - - if (workersNumber > 30 && (gameState.currentPhase() > 1 || gameState.isResearching("phase_town_generic") - || gameState.isResearching("phase_town_athens"))) { - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.totalLength() < 1) { - debug ("Trying to build barracks"); - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); - } - } - - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 2 && workersNumber > 85) - if (queues.militaryBuilding.totalLength() < 1) - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); - - if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) === 2 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 3 && workersNumber > 125) - if (queues.militaryBuilding.totalLength() < 1) - { - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); - if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); - } - } - //build advanced military buildings - if (workersNumber >= 75 && gameState.currentPhase() > 2){ - if (queues.militaryBuilding.totalLength() === 0){ - var inConst = 0; - for (var i in this.bAdvanced) - inConst += gameState.countFoundationsWithType(gameState.applyCiv(this.bAdvanced[i])); - if (inConst == 0 && this.bAdvanced && this.bAdvanced.length !== 0) { - var i = Math.floor(Math.random() * this.bAdvanced.length); - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); - } - } - } - } - if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && - workersNumber > 130 && gameState.currentPhase() > 2) - { - var Const = 0; - for (var i in this.bAdvanced) - Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i])); - if (inConst == 1) { - var i = Math.floor(Math.random() * this.bAdvanced.length); - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); - queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); - } - } - } - - Engine.ProfileStop(); -}; - -// TODO: use pop(). Currently unused as this is too gameable. -MilitaryAttackManager.prototype.garrisonAllFemales = function(gameState) { - var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); - var females = gameState.getOwnEntities().filter(Filters.byClass("Support")); - - var cache = {}; - - females.forEach( function (ent) { - for (var i in buildings) - { - if (ent.position()) - { - var struct = buildings[i]; - if (!cache[struct.id()]) - cache[struct.id()] = 0; - if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) - { - ent.garrison(struct); - cache[struct.id()]++; - break; - } - } - } - }); - this.hasGarrisonedFemales = true; -}; -MilitaryAttackManager.prototype.ungarrisonAll = function(gameState) { - this.hasGarrisonedFemales = false; - var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); - buildings.forEach( function (struct) { - if (struct.garrisoned() && struct.garrisoned().length) - struct.unloadAll(); - }); -}; - -MilitaryAttackManager.prototype.pausePlan = function(gameState, planName) { - for (var attackType in this.upcomingAttacks) { - for (var i in this.upcomingAttacks[attackType]) { - var attack = this.upcomingAttacks[attackType][i]; - if (attack.getName() == planName) - attack.setPaused(gameState, true); - } - } - for (var attackType in this.startedAttacks) { - for (var i in this.startedAttacks[attackType]) { - var attack = this.startedAttacks[attackType][i]; - if (attack.getName() == planName) - attack.setPaused(gameState, true); - } - } -} -MilitaryAttackManager.prototype.unpausePlan = function(gameState, planName) { - for (var attackType in this.upcomingAttacks) { - for (var i in this.upcomingAttacks[attackType]) { - var attack = this.upcomingAttacks[attackType][i]; - if (attack.getName() == planName) - attack.setPaused(gameState, false); - } - } - for (var attackType in this.startedAttacks) { - for (var i in this.startedAttacks[attackType]) { - var attack = this.startedAttacks[attackType][i]; - if (attack.getName() == planName) - attack.setPaused(gameState, false); - } - } -} -MilitaryAttackManager.prototype.pauseAllPlans = function(gameState) { - for (var attackType in this.upcomingAttacks) { - for (var i in this.upcomingAttacks[attackType]) { - var attack = this.upcomingAttacks[attackType][i]; - attack.setPaused(gameState, true); - } - } - for (var attackType in this.startedAttacks) { - for (var i in this.startedAttacks[attackType]) { - var attack = this.startedAttacks[attackType][i]; - attack.setPaused(gameState, true); - } - } -} -MilitaryAttackManager.prototype.unpauseAllPlans = function(gameState) { - for (var attackType in this.upcomingAttacks) { - for (var i in this.upcomingAttacks[attackType]) { - var attack = this.upcomingAttacks[attackType][i]; - attack.setPaused(gameState, false); - } - } - for (var attackType in this.startedAttacks) { - for (var i in this.startedAttacks[attackType]) { - var attack = this.startedAttacks[attackType][i]; - attack.setPaused(gameState, false); - } - } -} -MilitaryAttackManager.prototype.update = function(gameState, queues, events) { - var self = this; - - Engine.ProfileStart("military update"); - - this.gameState = gameState; - - Engine.ProfileStart("Constructing military buildings and building defences"); - this.constructTrainingBuildings(gameState, queues); - - if(gameState.getTimeElapsed() > this.defenceBuildingTime) - this.buildDefences(gameState, queues); - Engine.ProfileStop(); - - this.defenceManager.update(gameState, events, this); - - Engine.ProfileStart("Looping through attack plans"); - // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. - if (1) { - for (var attackType in this.upcomingAttacks) { - for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { - - var attack = this.upcomingAttacks[attackType][i]; - - // okay so we'll get the support plan - if (!attack.isStarted()) { - var updateStep = attack.updatePreparation(gameState, this,events); - - // now we're gonna check if the preparation time is over - if (updateStep === 1 || attack.isPaused() ) { - // just chillin' - } else if (updateStep === 0 || updateStep === 3) { - debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); - if (updateStep === 3) { - this.attackPlansEncounteredWater = true; - debug("No attack path found. Aborting."); - } - attack.Abort(gameState, this); - this.upcomingAttacks[attackType].splice(i--,1); - } else if (updateStep === 2) { - var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - if (Math.random() < 0.2) - chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - else if (Math.random() < 0.3) - chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - else if (Math.random() < 0.3) - chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - gameState.ai.chatTeam(chatText); - - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); - attack.StartAttack(gameState,this); - this.startedAttacks[attackType].push(attack); - this.upcomingAttacks[attackType].splice(i--,1); - } - } else { - var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - if (Math.random() < 0.2) - chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - else if (Math.random() < 0.3) - chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - else if (Math.random() < 0.3) - chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; - gameState.ai.chatTeam(chatText); - - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); - this.startedAttacks[attackType].push(attack); - this.upcomingAttacks[attackType].splice(i--,1); - } - } - } - } - for (var attackType in this.startedAttacks) { - for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { - var attack = this.startedAttacks[attackType][i]; - // okay so then we'll update the attack. - if (!attack.isPaused()) - { - var remaining = attack.update(gameState,this,events); - if (remaining == 0 || remaining == undefined) { - debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); - attack.Abort(gameState); - this.startedAttacks[attackType].splice(i--,1); - } - } - } - } - // Note: these indications of "rush" are currently unused. - if (gameState.ai.strategy === "rush" && this.startedAttacks["CityAttack"].length !== 0) { - // and then we revert. - gameState.ai.strategy = "normal"; - Config.Economy.femaleRatio = 0.4; - gameState.ai.modules.economy.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1); - } else if (gameState.ai.strategy === "rush" && this.upcomingAttacks["CityAttack"].length === 0) - { - Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "rush") - this.TotalAttackNumber++; - this.upcomingAttacks["CityAttack"].push(Lalala); - debug ("Starting a little something"); - } else if (gameState.ai.strategy !== "rush") - { - // creating plans after updating because an aborted plan might be reused in that case. - if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater - && gameState.getTimeElapsed() > this.attackPlansStartTime) { - if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock")) === 0 && gameState.ai.waterMap) - { - // wait till we get a dock. - } else { - // basically only the first plan, really. - if (this.upcomingAttacks["CityAttack"].length == 0 && (gameState.getTimeElapsed() < 12*60000 || Config.difficulty < 1)) { - var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1); - if (Lalala.failed) - { - this.attackPlansEncounteredWater = true; // hack - } else { - debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); - this.TotalAttackNumber++; - this.upcomingAttacks["CityAttack"].push(Lalala); - } - } else if (this.upcomingAttacks["CityAttack"].length == 0 && Config.difficulty !== 0) { - var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized"); - if (Lalala.failed) - { - this.attackPlansEncounteredWater = true; // hack - } else { - debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); - this.TotalAttackNumber++; - this.upcomingAttacks["CityAttack"].push(Lalala); - } - } - } - } - } - - /* - // very old relic. This should be reimplemented someday so the code stays here. - - if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { - var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); - if (!Lalala.createSupportPlans(gameState, this, )) { - debug ("Military Manager: harrassing plan not a valid option"); - this.HarassRaiding = false; - } else { - debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); - - this.totalStartedAttackNumber++; - this.preparingRaidNumber++; - this.currentAttacks.push(Lalala); - } - } - */ - - - Engine.ProfileStop(); - Engine.ProfileStop(); -}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js b/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js new file mode 100644 index 0000000000..fb36c4a713 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js @@ -0,0 +1,288 @@ +/* Naval Manager + Will deal with anything ships. + -Basically trade over water (with fleets and goals commissioned by the economy manager) + -Defence over water (commissioned by the defense manager) + -subtask being patrols, escort, naval superiority. + -Transport of units over water (a few units). + -Scouting, ultimately. + Also deals with handling docks, making sure we have access and stuffs like that. + Does not build them though, that's for the base manager to handle. + */ + +var NavalManager = function() { + // accessibility zones for which we have a dock. + // Connexion is described as [landindex] = [seaIndexes]; + // technically they also exist for sea zones but I don't care. + this.landZoneDocked = []; + + // list of seas I have a dock on. + this.accessibleSeas = []; + + // ship subCollections. Also exist for land zones, idem, not caring. + this.seaShips = []; + this.seaTpShips = []; + this.seaWarships = []; + + // wanted NB per zone. + this.wantedTpShips = []; + this.wantedWarships = []; + + this.transportPlans = []; + this.askedPlans = []; +}; + +// More initialisation for stuff that needs the gameState +NavalManager.prototype.init = function(gameState, events, queues) { + // finished docks + this.docks = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Dock"), Filters.not(Filters.isFoundation()))); + this.docks.allowQuickIter(); + this.docks.registerUpdates(); + + this.ships = gameState.getOwnEntities().filter(Filters.byClass("Ship")); + // note: those two can overlap (some transport ships are warships too and vice-versa). + this.tpShips = this.ships.filter(Filters.byCanGarrison()); + this.warships = this.ships.filter(Filters.byClass("Warship")); + + this.ships.registerUpdates(); + this.tpShips.registerUpdates(); + this.warships.registerUpdates(); + + for (var i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) + { + if (gameState.ai.accessibility.regionType[i] !== "water") + { + // push dummies + this.seaShips.push(new EntityCollection(gameState.sharedScript)); + this.seaTpShips.push(new EntityCollection(gameState.sharedScript)); + this.seaWarships.push(new EntityCollection(gameState.sharedScript)); + this.wantedTpShips.push(0); + this.wantedWarships.push(0); + } else { + var collec = this.ships.filter(Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaShips.push(collec); + collec = this.tpShips.filter(Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaTpShips.push(collec); + var collec = this.warships.filter(Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaWarships.push(collec); + + this.wantedTpShips.push(1); + this.wantedWarships.push(1); + } + + this.landZoneDocked.push([]); + } +}; + +NavalManager.prototype.getUnconnectedSeas = function (gameState, region) { + var seas = gameState.ai.accessibility.regionLinks[region] + if (seas.length === 0) + return []; + for (var i = 0; i < seas.length; ++i) + { + if (this.landZoneDocked[region].indexOf(seas[i]) !== -1) + seas.splice(i--,1); + } + return seas; +}; + +// returns true if there is a path from A to B and we have docks. +NavalManager.prototype.canReach = function (gameState, regionA, regionB) { + var path = gameState.ai.accessibility.getTrajectToIndex(regionA, regionB); + if (!path) + { + return false; + } + for (var i = 0; i < path.length - 1; ++i) + { + if (gameState.ai.accessibility.regionType[path[i]] == "land") + if (this.accessibleSeas.indexOf(path[i+1]) === -1) + { + debug ("cannot reach because of " + path[i+1]); + return false; // we wn't be able to board on that sea + } + } + return true; +}; + + +NavalManager.prototype.checkEvents = function (gameState, queues, events) { + for (i in events) + { + if (events[i].type == "Destroy") + { + // TODO: probably check stuffs like a base destruction. + } else if (events[i].type == "ConstructionFinished") + { + var evt = events[i]; + if (evt.msg && evt.msg.newentity) + { + var entity = gameState.getEntityById(evt.msg.newentity); + if (entity && entity.hasClass("Dock") && entity.isOwn(PlayerID)) + { + // okay we have a dock whose construction is finished. + // let's assign it to us. + var pos = entity.position(); + var li = gameState.ai.accessibility.getAccessValue(pos); + var ni = entity.getMetadata(PlayerID, "sea"); + if (this.landZoneDocked[li].indexOf(ni) === -1) + this.landZoneDocked[li].push(ni); + if (this.accessibleSeas.indexOf(ni) === -1) + this.accessibleSeas.push(ni); + } + } + } + } +}; + +NavalManager.prototype.addPlan = function(plan) { + this.transportPlans.push(plan); +}; + +// will create a plan at the end of the turn. +// many units can call this separately and end up in the same plan +// which can be useful. +NavalManager.prototype.askForTransport = function(entity, startPos, endPos) { + this.askedPlans.push([entity, startPos, endPos]); +}; + +// creates aforementionned plans +NavalManager.prototype.createPlans = function(gameState) { + var startID = {}; + + for (i in this.askedPlans) + { + var plan = this.askedPlans[i]; + var startIndex = gameState.ai.accessibility.getAccessValue(plan[1]); + var endIndex = gameState.ai.accessibility.getAccessValue(plan[2]); + if (startIndex === 1 || endIndex === -1) + continue; + if (!startID[startIndex]) + { + startID[startIndex] = {}; + startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]}; + } + else if (!startID[startIndex][endIndex]) + startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]}; + else + startID[startIndex][endIndex].units.push(plan[0]); + } + for (var i in startID) + for (var k in startID[i]) + { + var tpPlan = new TransportPlan(gameState, startID[i][k].units, startID[i][k].dest, false) + this.transportPlans.push (tpPlan); + } +}; + +// TODO: work on this. +NavalManager.prototype.maintainFleet = function(gameState, queues, events) { + // check if we have enough transport ships. + // check per region. + for (var i = 0; i < this.seaShips.length; ++i) + { + var tpNb = gameState.countOwnQueuedEntitiesWithMetadata("sea", i); + if (this.accessibleSeas.indexOf(i) !== -1 && this.seaTpShips[i].length < this.wantedTpShips[i] + && tpNb + queues.ships.length() === 0 && gameState.getTemplate(gameState.applyCiv("units/{civ}_ship_bireme")).available(gameState)) + { + // TODO: check our dock can build the wanted ship types, for Carthage. + queues.ships.addItem(new TrainingPlan(gameState, "units/{civ}_ship_bireme", { "sea" : i }, 1, 0, -1, 1 )); + } + } +}; + +// bumps up the number of ships we want if we need more. +NavalManager.prototype.checkLevels = function(gameState, queues) { + if (queues.ships.length() !== 0) + return; + for (var i = 0; i < this.transportPlans.length; ++i) + { + var plan = this.transportPlans[i]; + if (plan.needTpShips()) + { + var zone = plan.neededShipsZone(); + if (zone && gameState.countOwnQueuedEntitiesWithMetadata("sea", zone) > 0) + continue; + if (zone && this.wantedTpShips[i] === 0) + this.wantedTpShips[i]++; + else if (zone && plan.allAtOnce) + this.wantedTpShips[i]++; + } + } +}; + +// assigns free ships to plans that need some +NavalManager.prototype.assignToPlans = function(gameState, queues, events) { + for (var i = 0; i < this.transportPlans.length; ++i) + { + var plan = this.transportPlans[i]; + if (plan.needTpShips()) + { + // assign one per go. + var zone = plan.neededShipsZone(); + if (zone) + { + for each (ship in this.seaTpShips[zone]._entities) + { + if (!ship.getMetadata(PlayerID, "tpplan")) + { + debug ("Assigning ship " + ship.id() + " to plan" + plan.ID); + plan.assignShip(gameState, ship); + return true; + } + } + } + } + } + return false; +}; + +NavalManager.prototype.checkActivePlan = function(ID) { + for (var i = 0; i < this.transportPlans.length; ++i) + if (this.transportPlans[i].ID === ID) + return true; + + return false; +}; + +// Some functions are run every turn +// Others once in a while +NavalManager.prototype.update = function(gameState, queues, events) { + Engine.ProfileStart("Naval Manager update"); + + this.checkEvents(gameState, queues, events); + + if (gameState.ai.playedTurn % 10 === 0) + { + this.maintainFleet(gameState, queues, events); + this.checkLevels(gameState, queues); + } + + for (var i = 0; i < this.transportPlans.length; ++i) + if (!this.transportPlans[i].carryOn(gameState, this)) + { + // whatever the reason, this plan needs to be ended + // it could be that it's finished though. + var seaZone = this.transportPlans[i].neededShipsZone(); + + var rallyPos = []; + this.docks.forEach(function (dock) { + if (dock.getMetadata(PlayerID,"sea") == seaZone) + rallyPos = dock.position(); + }); + this.transportPlans[i].ships.move(rallyPos); + this.transportPlans[i].releaseAll(gameState); + this.transportPlans.splice(i,1); + --i; + } + + this.assignToPlans(gameState, queues, events); + if (gameState.ai.playedTurn % 10 === 2) + { + this.createPlans(gameState); + this.askedPlans = []; + } + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js b/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js new file mode 100644 index 0000000000..73592956cf --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js @@ -0,0 +1,472 @@ +/* + Describes a transport plan + Constructor assign units (units is an ID array, or an ID), a destionation (position, ingame), and a wanted escort size. + If "onlyIfOk" is true, then the plan will only start if the wanted escort size is met. + 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 (relying on in-game pathfinder for mvt) + And then carry units from there. + If units are over multiple accessibility indexes (ie different islands) it will first group them + + 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. +*/ + +// TODO: finish the support of multiple accessibility indexes. +// TODO: this doesn't check we can actually reach in the init, which we might want? + +var TransportPlan = function(gameState, units, destination, allAtOnce, escortSize, onlyIfOK) { + var self = this; + + this.ID = uniqueIDTPlans++; + + var unitsID = []; + if (units.length !== undefined) + unitsID = units; + else + unitsID = [units]; + + this.units = EntityCollectionFromIds(gameState, unitsID); + this.units.forEach(function (ent) { //}){ + ent.setMetadata(PlayerID, "tpplan", self.ID); + ent.setMetadata(PlayerID, "formerRole", ent.getMetadata(PlayerID, "role")); + ent.setMetadata(PlayerID, "role", "transport"); + }); + + this.units.freeze(); + this.units.registerUpdates(); + + debug ("Starting a new plan with ID " + this.ID + " to " + destination); + debug ("units are " + uneval (units)); + + this.destination = destination; + this.destinationIndex = gameState.ai.accessibility.getAccessValue(destination); + + if (allAtOnce) + this.allAtOnce = allAtOnce; + else + this.allAtOnce = false; + + if (escortSize) + this.escortSize = escortSize; + else + this.escortSize = 0; + + if (onlyIfOK) + this.onlyIfOK = onlyIfOK; + else + this.onlyIfOK = false; + + this.state = "unstarted"; + + this.ships = gameState.ai.HQ.navalManager.ships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + // note: those two can overlap (some transport ships are warships too and vice-versa). + this.transportShips = gameState.ai.HQ.navalManager.tpShips.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + this.escortShips = gameState.ai.HQ.navalManager.warships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + + this.ships.registerUpdates(); + this.transportShips.registerUpdates(); + this.escortShips.registerUpdates(); +}; + +// count available slots +TransportPlan.prototype.countFreeSlots = function(onlyTrulyFree) +{ + var slots = 0; + this.transportShips.forEach(function (ent) { //}){ + slots += ent.garrisonMax(); + if (onlyTrulyFree) + slots -= ent.garrisoned().length; + }); +} + +TransportPlan.prototype.assignShip = function(gameState, ship) +{ + ship.setMetadata(PlayerID,"tpplan", this.ID); +} + +TransportPlan.prototype.releaseAll = function(gameState) +{ + this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) }); + this.units.forEach(function (ent) { + var fRole = ent.getMetadata(PlayerID, "formerRole"); + if (fRole) + ent.setMetadata(PlayerID,"role", fRole); + ent.setMetadata(PlayerID,"tpplan", undefined) + }); +} + +TransportPlan.prototype.releaseAllShips = function(gameState) +{ + this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) }); +} + +TransportPlan.prototype.needTpShips = function() +{ + if ((this.allAtOnce && this.countFreeSlots() >= this.units.length) || this.transportShips.length > 0) + return false; + return true; +} + +TransportPlan.prototype.needEscortShips = function() +{ + return !((this.onlyIfOK && this.escortShips.length < this.escortSize) || !this.onlyIfOK); +} + +// returns the zone for which we are needing our ships +TransportPlan.prototype.neededShipsZone = function() +{ + if (!this.seaZone) + return false; + return this.seaZone; +} + + +// try to move on. +/* several states: + "unstarted" is the initial state, and will determine wether we follow basic or grouping path + Basic path: + - "waitingForBoarding" means we wait 'till we have enough transport ships and escort ships to move stuffs. + - "Boarding" means we're trying to board units onto our ships + - "Moving" means we're moving ships + - "Unboarding" means we're unbording + - Once we're unboarded, we either return to boarding point (if we still have units to board) or we clear. + > there is the possibility that we'll be moving units on land, but that's basically a restart too, with more clearing. + Grouping Path is basically the same with "grouping" and we never unboard (unless there is a need to) + */ +TransportPlan.prototype.carryOn = function(gameState, navalManager) +{ + if (this.state === "unstarted") + { + // Okay so we can start the plan. + // So what we'll do is check what accessibility indexes our units are. + var unitIndexes = []; + this.units.forEach( function (ent) { //}){ + var idx = gameState.ai.accessibility.getAccessValue(ent.position()); + if (unitIndexes.indexOf(idx) === -1 && idx !== 1) + unitIndexes.push(idx); + }); + + // we have indexes. If there is more than 1, we'll try and regroup them. + if (unitIndexes.length > 1) + { + warn("Transport Plan path is too complicated, aborting"); + return false; + /* + this.state = "waitingForGrouping"; + // get the best index for grouping, ie start by the one farthest away in terms of movement. + var idxLength = {}; + for (var i = 0; i < unitIndexes.length; ++i) + idxLength[unitIndexes[i]] = gameState.ai.accessibility.getTrajectToIndex(unitIndexes[i], this.destinationIndex).length; + var sortedArray = unitIndexes.sort(function (a,b) { //}){ + return idxLength[b] - idxLength[a]; + }); + this.startIndex = sortedArray[0]; + // okay so we'll board units from this index and we'll try to join them with units of the next index. + // this might not be terribly efficient but it won't be efficient anyhow. + return true;*/ + } + this.state = "waitingForBoarding"; + + // let's get our index this turn. + this.startIndex = unitIndexes[0]; + + debug ("plan " + this.ID + " from " + this.startIndex); + + return true; + } + if (this.state === "waitingForBoarding") + { + + if (!this.path) + { + this.path = gameState.ai.accessibility.getTrajectToIndex(this.startIndex, this.destinationIndex); + if (!this.path || this.path.length === 0 || this.path.length % 2 === 0) + return false; // TODO: improve error handling + if (this.path[0] !== this.startIndex) + { + warn ("Start point of the path is not the start index, aborting transport plan"); + return false; + } + // we have a path, register the first sea zone. + this.seaZone = this.path[1]; + debug ("Plan " + this.ID + " over seazone " + this.seaZone); + } + // if we currently have no baoarding spot, try and find one. + if (!this.boardingSpot) + { + // TODO: improve on this whenever we have danger maps. + // okay so we have units over an accessibility index. + // we'll get a map going on. + var Xibility = gameState.ai.accessibility; + + // custom obstruction map that uses the shore as the obstruction map + // but doesn't really check like for a building. + // created realtime with the other map. + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.ai.territoryMap; + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore"); + var obstructions = new Map(gameState.sharedScript); + + // wanted map. + var friendlyTiles = new Map(gameState.sharedScript); + + for (var j = 0; j < friendlyTiles.length; ++j) + { + // only on the wanted island + if (Xibility.landPassMap[j] !== this.startIndex) + continue; + + // setting obstructions + var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK); + // invalid is enemy-controlled or not on the right sea/land (we need a shore for this, we might want to check neighbhs instead). + var invalidTerritory = (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) + || (Xibility.navalPassMap[j] !== this.path[1]); + obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255; + + // currently we'll just like better on our territory + if (tilePlayer == PlayerID) + friendlyTiles.map[j] = 100; + } + + obstructions.expandInfluences(); + + var best = friendlyTiles.findBestTile(4, obstructions); + var bestIdx = best[0]; + + // not good enough. + if (best[1] <= 0) + { + best = friendlyTiles.findBestTile(1, obstructions); + bestIdx = best[0]; + if (best[1] <= 0) + return false; // apparently we won't be able to board. + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + // we have the spot we want to board at. + this.boardingSpot = [x,z]; + debug ("Plan " + this.ID + " new boarding spot is " + this.boardingSpot); + } + + // if all at once we need to be full, else we just need enough escort ships. + if (!this.needTpShips() && !this.needEscortShips()) + { + // preparing variables + // TODO: destroy former entity collection. + this.garrisoningUnits = this.units.filter(Filters.not(Filters.isGarrisoned())); + this.garrisoningUnits.registerUpdates(); + this.garrisoningUnits.freeze(); + + this.garrisonShipID = -1; + + debug ("Boarding"); + this.state = "boarding"; + } + return true; + } else if (this.state === "waitingForGrouping") + { + // TODO: this. + return true; + } + if (this.state === "boarding" && gameState.ai.playedTurn % 5 === 0) + { + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + if (!this.boardingSpot) + return false; + if (this.needTpShips()) + { + this.state = "waitingForBoarding"; + return true; + } + if (this.needEscortShips()) + { + this.state = "waitingForBoarding"; + return true; + } + + // check if we aren't actually finished. + if (this.units.getCentrePosition() == undefined || this.countFreeSlots(true) === 0) + { + delete this.boardingSpot; + this.garrisoningUnits.unregister(); + this.state = "moving"; + return true; + } + + // check if we need to move our units and ships closer together + var stillMoving = false; + if (SquareVectorDistance(this.ships.getCentrePosition(),this.boardingSpot) > 1600) + { + this.ships.move(this.boardingSpot[0],this.boardingSpot[1]); + stillMoving = true; // wait till ships are in position + } + if (SquareVectorDistance(this.units.getCentrePosition(),this.boardingSpot) > 1600) + { + this.units.move(this.boardingSpot[0],this.boardingSpot[1]); + stillMoving = true; // wait till units are in position + } + if (stillMoving) + { + return true; // wait. + } + // check if we need to try and board units. + var garrisonShip = gameState.getEntityById(this.garrisonShipID); + var self = this; + // check if ship we're currently garrisoning in is full + if (garrisonShip && garrisonShip.canGarrisonInside()) + { + // okay garrison units + var nbStill = garrisonShip.garrisonMax() - garrisonShip.garrisoned().length; + if (this.garrisoningUnits.length < nbStill) + { + Engine.PostCommand({"type": "garrison", "entities": this.garrisoningUnits.toIdArray(), "target": garrisonShip.id(),"queued": false}); + } + return true; + } else if (garrisonShip) + { + // full ship, abort + this.garrisonShipID = -1; + garrisonShip = false; // will enter next if. + } + if (!garrisonShip) + { + // could have died or could have be full + // we'll pick a new one, one that isn't full + for (i in this.transportShips._entities) + { + if (this.transportShips._entities[i].canGarrisonInside()) + { + this.garrisonShipID = this.transportShips._entities[i].id(); + break; + } + } + return true; // wait. + } + // could I actually get here? + return true; + } + if (this.state === "moving") + { + if (!this.unboardingSpot) + { + // TODO: improve on this whenever we have danger maps. + // okay so we have units over an accessibility index. + // we'll get a map going on. + var Xibility = gameState.ai.accessibility; + + // custom obstruction map that uses the shore as the obstruction map + // but doesn't really check like for a building. + // created realtime with the other map. + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.ai.territoryMap; + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore"); + var obstructions = new Map(gameState.sharedScript); + + // wanted map. + var friendlyTiles = new Map(gameState.sharedScript); + + var wantedIndex = -1; + + if (this.path.length >= 3) + { + this.path.splice(0,2); + wantedIndex = this.path[0]; + } else { + debug ("too short at " +uneval(this.path)); + return false; // Incomputable + } + + for (var j = 0; j < friendlyTiles.length; ++j) + { + // only on the wanted island + if (Xibility.landPassMap[j] !== wantedIndex) + continue; + + // setting obstructions + var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK); + // invalid is not on the right land (we need a shore for this, we might want to check neighbhs instead). + var invalidTerritory = (Xibility.landPassMap[j] !== wantedIndex); + obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255; + + // currently we'll just like better on our territory + if (tilePlayer == PlayerID) + friendlyTiles.map[j] = 100; + else if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) + friendlyTiles.map[j] = 4; + else + friendlyTiles.map[j] = 50; + } + + obstructions.expandInfluences(); + + var best = friendlyTiles.findBestTile(4, obstructions); + var bestIdx = best[0]; + + // not good enough. + if (best[1] <= 0) + { + best = friendlyTiles.findBestTile(1, obstructions); + bestIdx = best[0]; + if (best[1] <= 0) + return false; // apparently we won't be able to unboard. + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + // we have the spot we want to board at. + this.unboardingSpot = [x,z]; + return true; + } + + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + if (!this.unboardingSpot) + return false; + + // check if we need to move ships + if (SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + { + this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]); + } else { + this.state = "unboarding"; + return true; + } + return true; + } + if (this.state === "unboarding") + { + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + + // check if we need to move ships + if (SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + { + this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]); + } else { + this.transportShips.forEach( function (ent) { ent.unloadAll() }); + // TODO: improve on this. + if (this.path.length > 1) + { + debug ("plan " + this.ID + " going back for more"); + // basically reset. + delete this.boardingSpot; + delete this.unboardingSpot; + this.state = "unstarted"; + this.releaseAllShips(); + return true; + } + debug ("plan " + this.ID + " is finished"); + return false; + } + } + + return true; +} diff --git a/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js b/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js index 1d3cebd121..e40593bc98 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js +++ b/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js @@ -15,8 +15,6 @@ // 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. // -// The fact that there is an outqueue is mostly a relic of qBot. -// // This system should be improved. It's probably not flexible enough. var QueueManager = function(queues, priorities) { @@ -36,7 +34,6 @@ var QueueManager = function(queues, priorities) { this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) }); this.curItemQueue = []; - }; QueueManager.prototype.getAvailableResources = function(gameState, noAccounts) { @@ -49,54 +46,217 @@ QueueManager.prototype.getAvailableResources = function(gameState, noAccounts) { return resources; }; -QueueManager.prototype.futureNeeds = function(gameState, EcoManager) { +QueueManager.prototype.getTotalAccountedResources = function(gameState) { + var resources = new Resources(); + for (var key in this.queues) { + resources.add(this.accounts[key]); + } + return resources; +}; + +QueueManager.prototype.currentNeeds = function(gameState) { var needs = new Resources(); - // get ouy current resources, not removing accounts. + // get out current resources, not removing accounts. var current = this.getAvailableResources(gameState, true); //queueArrays because it's faster. for (var i in this.queueArrays) { var name = this.queueArrays[i][0]; var queue = this.queueArrays[i][1]; - for (var j = 0; j < Math.min(2,queue.length()); ++j) + if (queue.length() > 0 && queue.getNext().isGo(gameState)) + needs.add(queue.getNext().getCost()); + else if (queue.length() > 0 && !queue.getNext().isGo(gameState)) { - needs.add(queue.queue[j].getCost()); + var cost = queue.getNext().getCost(); + cost.multiply(0.5); + needs.add(cost); } + if (queue.length() > 1 && queue.queue[1].isGo(gameState)) + needs.add(queue.queue[1].getCost()); } - if (EcoManager === false) { - return { - "food" : Math.max(needs.food - current.food, 0), - "wood" : Math.max(needs.wood - current.wood, 0), - "stone" : Math.max(needs.stone - current.stone, 0), - "metal" : Math.max(needs.metal - current.metal, 0) - }; - } else { - // Return predicted values minus the current stockpiles along with a base rater for all resources - return { - "food" : (Math.max(needs.food - current.food, 0) + EcoManager.baseNeed["food"])/2, - "wood" : (Math.max(needs.wood - current.wood, 0) + EcoManager.baseNeed["wood"])/2, - "stone" : (Math.max(needs.stone - current.stone, 0) + EcoManager.baseNeed["stone"])/2, - "metal" : (Math.max(needs.metal - current.metal, 0) + EcoManager.baseNeed["metal"])/2 - }; - } + return { + "food" : Math.max(25 + needs.food - current.food, 0), + "wood" : Math.max(needs.wood - current.wood, 0), + "stone" : Math.max(needs.stone - current.stone, 0), + "metal" : Math.max(needs.metal - current.metal, 0) + }; }; -QueueManager.prototype.printQueues = function(gameState){ - debug("OUTQUEUES"); - for (var i in this.queues){ - var qStr = ""; - var q = this.queues[i]; - if (q.outQueue.length > 0) - debug((i + ":")); - for (var j in q.outQueue){ - qStr = " " + q.outQueue[j].type + " "; - if (q.outQueue[j].number) - qStr += "x" + q.outQueue[j].number; - debug (qStr); +QueueManager.prototype.futureNeeds = function(gameState) { + var needs = new Resources(); + // get out current resources, not removing accounts. + var current = this.getAvailableResources(gameState, true); + //queueArrays because it's faster. + for (var i in this.queueArrays) + { + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + for (var j = 0; j < queue.length(); ++j) + { + var costs = queue.queue[j].getCost(); + if (!queue.queue[j].isGo(gameState)) + costs.multiply(0.5); + needs.add(costs); } } + return { + "food" : Math.max(25 + needs.food - current.food, 10), + "wood" : Math.max(needs.wood - current.wood, 10), + "stone" : Math.max(needs.stone - current.stone, 0), + "metal" : Math.max(needs.metal - current.metal, 0) + }; +}; + +// calculate the gather rates we'd want to be able to use all elements in our queues +QueueManager.prototype.wantedGatherRates = function(gameState) { + var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + var qTime = gameState.getTimeElapsed(); + var qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + + var currentRess = this.getAvailableResources(gameState); - debug("INQUEUES"); + //queueArrays because it's faster. + for (var i in this.queueArrays) + { + qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + qTime = gameState.getTimeElapsed(); + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + + for (var j = 0; j < queue.length(); ++j) + { + var elem = queue.queue[j]; + var cost = elem.getCost(); + if (qTime < elem.startTime) + qTime = elem.startTime; + if (!elem.isGo(gameState)) + { + // assume 2 minutes. + // TODO work on this. + for (type in qCosts) + qCosts[type] += cost[type]; + qTime += 120000; + break; // disregard other stuffs. + } + if (!elem.endTime) + { + // estimate time based on priority + cost + nb + // TODO: work on this. + for (type in qCosts) + qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])); + qTime += 30000; + } else { + // TODO: work on this. + for (type in qCosts) + qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])); + // TODO: refine based on % completed. + qTime += (elem.endTime-elem.startTime); + } + } + for (j in qCosts) + { + qCosts[j] -= this.accounts[name][j]; + var diff = Math.min(qCosts[j], currentRess[j]); + qCosts[j] -= diff; + currentRess[j] -= diff; + rates[j] += qCosts[j]/(qTime/1000); + } + } + return rates; +}; + +/*QueueManager.prototype.logNeeds = function(gameState) { + if (!this.totor) + { + this.totor = []; + this.currentGathR = []; + this.currentGathRWanted = []; + this.ressLev = []; +} + + if (gameState.ai.playedTurn % 10 !== 0) + return; + + + var array = this.wantedGatherRates(gameState); + this.totor.push( array ); + + + var currentRates = {}; + for (var type in array) + currentRates[type] = 0; + for (i in gameState.ai.HQ.baseManagers) + { + var base = gameState.ai.HQ.baseManagers[i]; + for (var type in array) + { + base.gatherersByType(gameState,type).forEach (function (ent) { //}){ + var worker = ent.getMetadata(PlayerID, "worker-object"); + if (worker) + currentRates[type] += worker.getGatherRate(gameState); + }); + } + } + this.currentGathR.push( currentRates ); + + var types = Object.keys(array); + + types.sort(function(a, b) { + var va = (Math.max(0,array[a] - currentRates[a]))/ (currentRates[a]+1); + var vb = (Math.max(0,array[b] - currentRates[b]))/ (currentRates[b]+1); + if (va === vb) + return (array[b]/(currentRates[b]+1)) - (array[a]/(currentRates[a]+1)); + return vb-va; + }); + this.currentGathRWanted.push( types ); + + var rss = gameState.getResources(); + this.ressLev.push( {"food" : rss["food"],"stone" : rss["stone"],"wood" : rss["wood"],"metal" : rss["metal"]} ); + + if (gameState.getTimeElapsed() > 20*60*1000 && !this.once) + { + this.once = true; + for (j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.totor[i][j] + ";"); + } + } + log(); + for (j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.currentGathR[i][j] + ";"); + } + } + log(); + for (j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.currentGathRWanted[i].indexOf(j) + ";"); + } + } + log(); + for (j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.ressLev[i][j] + ";"); + } + } + } +}; +*/ + +QueueManager.prototype.printQueues = function(gameState){ + debug("QUEUES"); for (var i in this.queues){ var qStr = ""; var q = this.queues[i]; @@ -115,10 +275,56 @@ QueueManager.prototype.printQueues = function(gameState){ debug(p + ": " + uneval(this.accounts[p])); } debug("Needed Resources:" + uneval(this.futureNeeds(gameState,false))); + debug ("Wanted Gather Rates:" + uneval(this.wantedGatherRates(gameState))); debug ("Current Resources:" + uneval(gameState.getResources())); debug ("Available Resources:" + uneval(this.getAvailableResources(gameState))); }; +// nice readable HTML version. +QueueManager.prototype.HTMLprintQueues = function(gameState){ + if (!Config.debug) + return; + log(" Aegis Queue Manager "); + for (var i in this.queues){ + log (""); + + var q = this.queues[i]; + var str = ""); + for (var j in q.queue) { + if (q.queue[j].isGo(gameState)) + log (""); + } + log (""); + } + log ("
Aegis Build Order
" + i +"
"; + for each (k in this.accounts[i].types) + if(k != "population") + { + str += this.accounts[i][k] + k.substr(0,1).toUpperCase() ; + if (k != "metal") str += " / "; + } + log(str + "
"); + else + log (""); + + var qStr = ""; + qStr += q.queue[j].type; + if (q.queue[j].number) + qStr += "x" + q.queue[j].number; + log (qStr); + log ("
"); + /*log ("

Accounts

"); + for (var p in this.accounts) + { + log("

" + p + ": " + uneval(this.accounts[p]) + "

"); + }*/ + log ("

Needed Resources:" + uneval(this.futureNeeds(gameState,false)) + "

"); + log ("

Wanted Gather Rate:" + uneval(this.wantedGatherRates(gameState)) + "

"); + log ("

Current Resources:" + uneval(gameState.getResources()) + "

"); + log ("

Available Resources:" + uneval(this.getAvailableResources(gameState)) + "

"); + log(""); +}; + QueueManager.prototype.clear = function(){ this.curItemQueue = []; for (var i in this.queues) @@ -136,35 +342,9 @@ QueueManager.prototype.update = function(gameState) { } Engine.ProfileStart("Queue Manager"); - - //if (gameState.ai.playedTurn % 10 === 0) - // this.printQueues(gameState); - - Engine.ProfileStart("Pick items from queues"); - - // TODO: this only pushes the first object. SHould probably try to push any possible object to maximize productivity. Perhaps a settinh? - // looking at queues in decreasing priorities and pushing to the current item queues. - for (var i in this.queueArrays) - { - var name = this.queueArrays[i][0]; - var queue = this.queueArrays[i][1]; - if (queue.length() > 0) - { - var item = queue.getNext(); - var total = new Resources(); - total.add(this.accounts[name]); - total.subtract(queue.outQueueCost()); - if (total.canAfford(item.getCost())) - { - queue.nextToOutQueue(); - } - } else if (queue.totalLength() === 0) { - this.accounts[name].reset(); - } - } - + + // Let's assign resources to plans that need'em var availableRes = this.getAvailableResources(gameState); - // assign some accounts to queues. This is done by priority, and by need. for (var ress in availableRes) { if (availableRes[ress] > 0 && ress != "population") @@ -173,7 +353,7 @@ QueueManager.prototype.update = function(gameState) { var tempPrio = {}; var maxNeed = {}; // Okay so this is where it gets complicated. - // If a queue requires "ress" for the next elements (in the queue or the outqueue) + // If a queue requires "ress" 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: @@ -182,76 +362,156 @@ QueueManager.prototype.update = function(gameState) { // This avoids getting a high priority queue with many elements hogging all of one resource // uselessly while it awaits for other resources. for (var j in this.queues) { - var outQueueCost = this.queues[j].outQueueCost(); - var queueCost = this.queues[j].queueCost(); - if (this.accounts[j][ress] < queueCost[ress] + outQueueCost[ress]) + // returns exactly the correct amount, ie 0 if we're not go. + var queueCost = this.queues[j].maxAccountWanted(gameState); + if (this.queues[j].length() > 0 && this.accounts[j][ress] < queueCost[ress] && !this.queues[j].paused) { + // check that we're not too forward in this resource compared to others. + /*var maxp = this.accounts[j][ress] / (queueCost[ress]+1); + var tooFull = false; + for (tempRess in availableRes) + if (tempRess !== ress && queueCost[tempRess] > 0 && (this.accounts[j][tempRess] / (queueCost[tempRess]+1)) - maxp < -0.2) + tooFull = true; + if (tooFull) + continue;*/ + // adding us to the list of queues that need an update. tempPrio[j] = this.priorities[j]; - maxNeed[j] = outQueueCost[ress] + this.queues[j].getNext().getCost()[ress]; - // if we have enough of that resource for the outqueue and our first resource in the queue, diminish our priority. - if (this.accounts[j][ress] >= outQueueCost[ress] + this.queues[j].getNext().getCost()[ress]) - { + maxNeed[j] = queueCost[ress] - this.accounts[j][ress]; + // if we have enough of that resource for our first item in the queue, diminish our priority. + if (this.accounts[j][ress] >= this.queues[j].getNext().getCost()[ress]) tempPrio[j] /= 2; - if (this.queues[j].length() !== 1) - { - var halfcost = this.queues[j].queue[1].getCost()[ress]*0.8; - maxNeed[j] += halfcost; - if (this.accounts[j][ress] >= outQueueCost[ress] + this.queues[j].getNext().getCost()[ress] + halfcost) - delete tempPrio[j]; - } - } + if (tempPrio[j]) totalPriority += tempPrio[j]; } + else if (this.accounts[j][ress] > queueCost[ress]) + { + this.accounts[j][ress] = queueCost[ress]; + } } // 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. for (var j in tempPrio) { // we'll add at much what can be allowed to this queue. - var toAdd = Math.floor(tempPrio[j]/totalPriority * availableRes[ress]); - // let's check we're not adding too much. - var maxAdd = Math.min(maxNeed[j] - this.accounts[j][ress], toAdd); + var toAdd = tempPrio[j]/totalPriority * availableRes[ress]; + var maxAdd = Math.floor(Math.min(maxNeed[j], toAdd)); this.accounts[j][ress] += maxAdd; } - } + }/* else if (ress != "population" && gameState.ai.playedTurn % 5 === 0) { + // okay here we haev no resource available. We'll try to shift resources to complete plans if possible. + // So basically if 2 queues have resources, and one is higher priority, and it needs resources + // We'll shift from the lower priority to the higher if we can complete it. + var queues = []; + for (var j in this.queues) { + if (this.queues[j].length() && this.queues[j].getNext().isGo(gameState) && this.accounts[j][ress] > 0) + queues.push(j); + } + if (queues.length > 1) + { + // we'll work from the bottom to the top. ie lowest priority will try to give to highest priority. + queues.sort(function (a,b) { return (self.priorities[a] < self.priorities[b]); }); + var under = 0, over = queues.length - 1; + while (under !== over) + { + var cost = this.queues[queues[over]].getNext().getCost()[ress]; + var totalCost = this.queues[queues[over]].maxAccountWanted(gameState)[ress]; + if (this.accounts[queues[over]] >= cost) + { + --over; // check the next one. + continue; + } + // need some discrepancy in priorities + if (this.priorities[queues[under]] < this.priorities[queues[over]] - 20) + { + if (this.accounts[queues[under]] + this.accounts[queues[over]] >= cost) + { + var amnt = cost - this.accounts[queues[over]]; + this.accounts[queues[under]] -= amnt; + this.accounts[queues[over]] += amnt; + --over; + debug ("Shifting " + amnt + " from " + queues[under] + " to " +queues[over]); + continue; + } else { + ++under; + continue; + } + } else { + break; + } + } + // okaaaay. + } + }*/ } - Engine.ProfileStop(); - - Engine.ProfileStart("Execute items"); - var units_Techs_passed = 0; - // Handle output queues by executing items where possible - for (var p in this.queueArrays) { - var name = this.queueArrays[p][0]; - var queue = this.queueArrays[p][1]; - var next = queue.outQueueNext(); - if (!next) - continue; - if (next.category === "building") { - if (gameState.buildingsBuilt == 0) { - if (next.canExecute(gameState)) { - this.accounts[name].subtract(next.getCost()) - //debug ("Starting " + next.type + " substracted " + uneval(next.getCost())) - queue.executeNext(gameState); - gameState.buildingsBuilt += 1; + Engine.ProfileStart("Pick items from queues"); + + //debug ("start"); + //debug (uneval(this.accounts)); + // Start the next item in the queue if we can afford it. + for (var i in this.queueArrays) + { + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + if (queue.length() > 0 && !queue.paused) + { + var item = queue.getNext(); + var total = new Resources(); + total.add(this.accounts[name]); + if (total.canAfford(item.getCost())) + { + if (item.canStart(gameState)) + { + this.accounts[name].subtract(item.getCost()); + queue.startNext(gameState); } } - } else { - if (units_Techs_passed < 2 && queue.outQueueNext().canExecute(gameState)){ - //debug ("Starting " + next.type + " substracted " + uneval(next.getCost())) - this.accounts[name].subtract(next.getCost()) - queue.executeNext(gameState); - units_Techs_passed++; - } + } else if (queue.length() === 0) { + this.accounts[name].reset(); } - if (units_Techs_passed >= 2) - continue; } + //debug (uneval(this.accounts)); + Engine.ProfileStop(); + + if (gameState.ai.playedTurn % 30 === 0) + this.HTMLprintQueues(gameState); + Engine.ProfileStop(); }; +QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { + if (this.queues[queue]) + { + this.queues[queue].paused = true; + if (scrapAccounts) + this.accounts[queue].reset(); + } +} + +QueueManager.prototype.unpauseQueue = function(queue) { + if (this.queues[queue]) + this.queues[queue].paused = false; +} + +QueueManager.prototype.pauseAll = function(scrapAccounts, but) { + for (var p in this.queues) + if (p != but) + { + if (scrapAccounts) + this.accounts[p].reset(); + this.queues[p].paused = true; + } +} + +QueueManager.prototype.unpauseAll = function(but) { + for (var p in this.queues) + if (p != but) + this.queues[p].paused = false; +} + + QueueManager.prototype.addQueue = function(queueName, priority) { if (this.queues[queueName] == undefined) { this.queues[queueName] = new Queue(); diff --git a/binaries/data/mods/public/simulation/ai/aegis/queue.js b/binaries/data/mods/public/simulation/ai/aegis/queue.js index 14f4ac0891..4f5c3e73ae 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/queue.js +++ b/binaries/data/mods/public/simulation/ai/aegis/queue.js @@ -4,15 +4,13 @@ var Queue = function() { this.queue = []; - this.outQueue = []; + this.paused = false; }; Queue.prototype.empty = function() { this.queue = []; - this.outQueue = []; }; - Queue.prototype.addItem = function(plan) { for (var i in this.queue) { @@ -33,18 +31,26 @@ Queue.prototype.getNext = function() { } }; -Queue.prototype.outQueueNext = function(){ - if (this.outQueue.length > 0) { - return this.outQueue[0]; +Queue.prototype.startNext = function(gameState) { + if (this.queue.length > 0) { + this.queue.shift().start(gameState); + return true; } else { - return null; + return false; } }; -Queue.prototype.outQueueCost = function(){ +// returns the maximal account we'll accept for this queue. +// Currently 100% of the cost of the first element and 80% of that of the second +Queue.prototype.maxAccountWanted = function(gameState) { var cost = new Resources(); - for (var key in this.outQueue){ - cost.add(this.outQueue[key].getCost()); + if (this.queue.length > 0 && this.queue[0].isGo(gameState)) + cost.add(this.queue[0].getCost()); + if (this.queue.length > 1 && this.queue[1].isGo(gameState)) + { + var costs = this.queue[1].getCost(); + costs.multiply(0.8); + cost.add(costs); } return cost; }; @@ -57,21 +63,6 @@ Queue.prototype.queueCost = function(){ return cost; }; -Queue.prototype.nextToOutQueue = function(){ - if (this.queue.length > 0){ - this.outQueue.push(this.queue.shift()); - } -}; - -Queue.prototype.executeNext = function(gameState) { - if (this.outQueue.length > 0) { - this.outQueue.shift().execute(gameState); - return true; - } else { - return false; - } -}; - Queue.prototype.length = function() { return this.queue.length; }; @@ -84,57 +75,23 @@ Queue.prototype.countQueuedUnits = function(){ return count; }; -Queue.prototype.countOutQueuedUnits = function(){ - var count = 0; - for (var i in this.outQueue){ - count += this.outQueue[i].number; - } - return count; -}; - -Queue.prototype.countTotalQueuedUnits = function(){ - var count = 0; - for (var i in this.queue){ - count += this.queue[i].number; - } - for (var i in this.outQueue){ - count += this.outQueue[i].number; - } - return count; -}; -Queue.prototype.countTotalQueuedUnitsWithClass = function(classe){ +Queue.prototype.countQueuedUnitsWithClass = function(classe){ var count = 0; for (var i in this.queue){ if (this.queue[i].template && this.queue[i].template.hasClass(classe)) count += this.queue[i].number; } - for (var i in this.outQueue){ - if (this.outQueue[i].template && this.outQueue[i].template.hasClass(classe)) - count += this.outQueue[i].number; - } return count; }; -Queue.prototype.countTotalQueuedUnitsWithMetadata = function(data,value){ +Queue.prototype.countQueuedUnitsWithMetadata = function(data,value){ var count = 0; for (var i in this.queue){ if (this.queue[i].metadata[data] && this.queue[i].metadata[data] == value) count += this.queue[i].number; } - for (var i in this.outQueue){ - if (this.outQueue[i].metadata[data] && this.outQueue[i].metadata[data] == value) - count += this.outQueue[i].number; - } return count; }; -Queue.prototype.totalLength = function(){ - return this.queue.length + this.outQueue.length; -}; - -Queue.prototype.outQueueLength = function(){ - return this.outQueue.length; -}; - Queue.prototype.countAllByType = function(t){ var count = 0; @@ -143,10 +100,5 @@ Queue.prototype.countAllByType = function(t){ count += this.queue[i].number; } } - for (var i = 0; i < this.outQueue.length; i++){ - if (this.outQueue[i].type === t){ - count += this.outQueue[i].number; - } - } return count; -}; \ No newline at end of file +}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/plan-building.js b/binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js similarity index 77% rename from binaries/data/mods/public/simulation/ai/aegis/plan-building.js rename to binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js index 0be0678776..598ffda3d2 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/plan-building.js +++ b/binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js @@ -1,35 +1,55 @@ -var BuildingConstructionPlan = function(gameState, type, position) { +var ConstructionPlan = function(gameState, type, metadata, startTime, expectedTime, position) { this.type = gameState.applyCiv(type); this.position = position; + this.metadata = metadata; + + this.ID = uniqueIDBOPlans++; + this.template = gameState.getTemplate(this.type); if (!this.template) { - this.invalidTemplate = true; - this.template = undefined; - debug("Cannot build " + this.type); - return; + return false; } + this.category = "building"; this.cost = new Resources(this.template.cost()); this.number = 1; // The number of buildings to build + + if (!startTime) + this.startTime = 0; + else + this.startTime = startTime; + + if (!expectedTime) + this.expectedTime = -1; + else + this.expectedTime = expectedTime; + return true; }; -BuildingConstructionPlan.prototype.canExecute = function(gameState) { - if (this.invalidTemplate){ - return false; - } +// return true if we willstart amassing resource for this plan +ConstructionPlan.prototype.isGo = function(gameState) { + return (gameState.getTimeElapsed() > this.startTime); +}; - // TODO: verify numeric limits etc - if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) +// checks other than resource ones. +// TODO: change this. +ConstructionPlan.prototype.canStart = function(gameState) { + if (gameState.buildingsBuilt > 0) return false; + // TODO: verify numeric limits etc + if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) + { + return false; + } var builders = gameState.findBuilders(this.type); return (builders.length != 0); }; -BuildingConstructionPlan.prototype.execute = function(gameState) { - +ConstructionPlan.prototype.start = function(gameState) { + var builders = gameState.findBuilders(this.type).toEntityArray(); // We don't care which builder we assign, since they won't actually @@ -39,45 +59,52 @@ BuildingConstructionPlan.prototype.execute = function(gameState) { var pos = this.findGoodPosition(gameState); if (!pos){ if (this.template.hasClass("Naval")) - gameState.ai.modules.economy.dockFailed = true; + gameState.ai.HQ.dockFailed = true; debug("No room to place " + this.type); return; } + if (this.template.hasClass("Naval")) + debug (pos); + gameState.buildingsBuilt++; if (gameState.getTemplate(this.type).buildCategory() === "Dock") { for (var angle = 0; angle < Math.PI * 2; angle += Math.PI/4) { - builders[0].construct(this.type, pos.x, pos.z, angle); + builders[0].construct(this.type, pos.x, pos.z, angle, this.metadata); } } else - builders[0].construct(this.type, pos.x, pos.z, pos.angle); + builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); }; -BuildingConstructionPlan.prototype.getCost = function() { - return this.cost; +ConstructionPlan.prototype.getCost = function() { + var costs = new Resources(); + costs.add(this.cost); + return costs; }; -BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { +ConstructionPlan.prototype.findGoodPosition = function(gameState) { var template = gameState.getTemplate(this.type); var cellSize = gameState.cellSize; // size of each tile // First, find all tiles that are far enough away from obstructions: - var obstructionMap = Map.createObstructionMap(gameState,template); + var obstructionMap = Map.createObstructionMap(gameState,0, template); - //obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png"); + //obstructionMap.dumpIm(template.buildCategory() + "_obstructions_pre.png"); if (template.buildCategory() !== "Dock") obstructionMap.expandInfluences(); + //obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png"); + // Compute each tile's closeness to friendly structures: - var friendlyTiles = new Map(gameState); + var friendlyTiles = new Map(gameState.sharedScript); var alreadyHasHouses = false; - + // If a position was specified then place the building as close to it as possible if (this.position){ var x = Math.round(this.position[0] / cellSize); @@ -116,7 +143,6 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { { friendlyTiles.addInfluence(x, z, 30, -50); } else if (template.genericName() == "House") { - friendlyTiles.addInfluence(x, z, Math.ceil(infl/2.0),infl); // houses are farther away from other buildings but houses friendlyTiles.addInfluence(x, z, Math.ceil(infl/4.0),-infl/2.0); // houses are farther away from other buildings but houses } else if (template.hasClass("GarrisonFortress")) { @@ -139,6 +165,11 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { } } }); + if (this.metadata && this.metadata.base !== undefined) + for (var base in gameState.ai.HQ.baseManagers) + if (base != this.metadata.base) + for (var j in gameState.ai.HQ.baseManagers[base].territoryIndices) + friendlyTiles.map[gameState.ai.HQ.baseManagers[base].territoryIndices[j]] = 0; } //friendlyTiles.dumpIm(template.buildCategory() + "_" +gameState.getTimeElapsed() + ".png", 200); @@ -149,7 +180,7 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { // also not for fields who can be stacked quite a bit var radius = 0; if (template.genericName() == "Field") - radius = Math.ceil(template.obstructionRadius() / cellSize) - 0.4; + radius = Math.ceil(template.obstructionRadius() / cellSize); else if (template.hasClass("GarrisonFortress")) radius = Math.ceil(template.obstructionRadius() / cellSize) + 2; else if (template.buildCategory() === "Dock") @@ -191,8 +222,8 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { var angle = 3*Math.PI/4; return { - "x" : x, - "z" : z, + "x" : x+2, + "z" : z+2, "angle" : angle }; }; diff --git a/binaries/data/mods/public/simulation/ai/aegis/plan-research.js b/binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js similarity index 60% rename from binaries/data/mods/public/simulation/ai/aegis/plan-research.js rename to binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js index 7fcc7422d0..a6dd26347b 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/plan-research.js +++ b/binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js @@ -1,33 +1,51 @@ -var ResearchPlan = function(gameState, type, rush) { +var ResearchPlan = function(gameState, type, startTime, expectedTime, rush) { this.type = type; + this.ID = uniqueIDBOPlans++; + this.template = gameState.getTemplate(this.type); if (!this.template || this.template.researchTime === undefined) { - this.invalidTemplate = true; - this.template = undefined; - debug ("Invalid research"); return false; } this.category = "technology"; this.cost = new Resources(this.template.cost(),0); this.number = 1; // Obligatory for compatibility + + if (!startTime) + this.startTime = 0; + else + this.startTime = startTime; + + if (!expectedTime) + this.expectedTime = -1; + else + this.expectedTime = expectedTime; + if (rush) this.rush = true; else this.rush = false; + return true; }; -ResearchPlan.prototype.canExecute = function(gameState) { - if (this.invalidTemplate) - return false; +// return true if we willstart amassing resource for this plan +ResearchPlan.prototype.isGo = function(gameState) { + return (gameState.getTimeElapsed() > this.startTime); +}; +ResearchPlan.prototype.canStart = function(gameState) { // also checks canResearch return (gameState.findResearchers(this.type).length !== 0); }; -ResearchPlan.prototype.execute = function(gameState) { +ResearchPlan.prototype.start = function(gameState) { var self = this; + + // TODO: this is special cased for "rush" technologies, ie the town phase + // which currently is a 100% focus. + gameState.ai.queueManager.unpauseAll(); + //debug ("Starting the research plan for " + this.type); var trainers = gameState.findResearchers(this.type).toEntityArray(); @@ -49,6 +67,8 @@ ResearchPlan.prototype.execute = function(gameState) { }; ResearchPlan.prototype.getCost = function(){ - return this.cost; + var costs = new Resources(); + costs.add(this.cost); + return costs; }; diff --git a/binaries/data/mods/public/simulation/ai/aegis/plan-training.js b/binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js similarity index 57% rename from binaries/data/mods/public/simulation/ai/aegis/plan-training.js rename to binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js index bd976bb714..23f3875073 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/plan-training.js +++ b/binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js @@ -1,27 +1,43 @@ -var UnitTrainingPlan = function(gameState, type, metadata, number, maxMerge) { +var TrainingPlan = function(gameState, type, metadata, number, startTime, expectedTime, maxMerge) { this.type = gameState.applyCiv(type); this.metadata = metadata; + this.ID = uniqueIDBOPlans++; + this.template = gameState.getTemplate(this.type); - if (!this.template) { - this.invalidTemplate = true; - this.template = undefined; - return; - } - this.category= "unit"; + if (!this.template) + return false; + + this.category = "unit"; this.cost = new Resources(this.template.cost(), this.template._template.Cost.Population); - if (!number){ + if (!number) this.number = 1; - }else{ + else this.number = number; - } + if (!maxMerge) this.maxMerge = 5; else this.maxMerge = maxMerge; + + if (!startTime) + this.startTime = 0; + else + this.startTime = startTime; + + if (!expectedTime) + this.expectedTime = -1; + else + this.expectedTime = expectedTime; + return true; }; -UnitTrainingPlan.prototype.canExecute = function(gameState) { +// return true if we willstart amassing resource for this plan +TrainingPlan.prototype.isGo = function(gameState) { + return (gameState.getTimeElapsed() > this.startTime); +}; + +TrainingPlan.prototype.canStart = function(gameState) { if (this.invalidTemplate) return false; @@ -32,8 +48,8 @@ UnitTrainingPlan.prototype.canExecute = function(gameState) { return (trainers.length != 0); }; -UnitTrainingPlan.prototype.execute = function(gameState) { - //warn("Executing UnitTrainingPlan " + uneval(this)); +TrainingPlan.prototype.start = function(gameState) { + //warn("Executing TrainingPlan " + uneval(this)); var self = this; var trainers = gameState.findTrainers(this.type).toEntityArray(); @@ -50,20 +66,21 @@ UnitTrainingPlan.prototype.execute = function(gameState) { bb += 0.9; return (aa - bb); }); - + if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) + this.metadata.base = trainers[0].getMetadata(PlayerID,"base"); trainers[0].train(this.type, this.number, this.metadata); } }; -UnitTrainingPlan.prototype.getCost = function(){ +TrainingPlan.prototype.getCost = function(){ var multCost = new Resources(); multCost.add(this.cost); multCost.multiply(this.number); return multCost; }; -UnitTrainingPlan.prototype.addItem = function(amount){ +TrainingPlan.prototype.addItem = function(amount){ if (amount === undefined) amount = 1; this.number += amount; -}; \ No newline at end of file +}; diff --git a/binaries/data/mods/public/simulation/ai/aegis/worker.js b/binaries/data/mods/public/simulation/ai/aegis/worker.js index 7b46816559..9d52db633c 100644 --- a/binaries/data/mods/public/simulation/ai/aegis/worker.js +++ b/binaries/data/mods/public/simulation/ai/aegis/worker.js @@ -6,10 +6,11 @@ var Worker = function(ent) { this.ent = ent; this.maxApproachTime = 45000; this.unsatisfactoryResource = false; // if true we'll reguarly check if we can't have better now. + this.baseID = 0; }; -Worker.prototype.update = function(gameState) { - +Worker.prototype.update = function(baseManager, gameState) { + this.baseID = baseManager.ID; var subrole = this.ent.getMetadata(PlayerID, "subrole"); if (!this.ent.position() || (this.ent.getMetadata(PlayerID,"fleeing") && gameState.getTimeElapsed() - this.ent.getMetadata(PlayerID,"fleeing") < 8000)){ @@ -19,14 +20,30 @@ Worker.prototype.update = function(gameState) { if (this.ent.getMetadata(PlayerID,"fleeing")) this.ent.setMetadata(PlayerID,"fleeing", undefined); + // 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 we're fighting, let's not start gathering, heh? + // TODO: remove this when we're hunting? + if (this.ent.unitAIState().split(".")[1] === "COMBAT" || this.ent.getMetadata(PlayerID, "role") === "transport") + { + return; + } + if (subrole === "gatherer") { - if (this.ent.unitAIState().split(".")[1] !== "GATHER" && this.ent.unitAIState().split(".")[1] !== "COMBAT" && this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE"){ - // TODO: handle combat for hunting animals - if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || + if (this.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. + if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type")){ Engine.ProfileStart("Start Gathering"); - this.startGathering(gameState); + this.unsatisfactoryResource = false; + this.startGathering(baseManager,gameState); Engine.ProfileStop(); + + this.startApproachingResourceTime = gameState.getTimeElapsed(); + } else { // Should deposit resources Engine.ProfileStart("Return Resources"); @@ -34,10 +51,12 @@ Worker.prototype.update = function(gameState) { { // no dropsite, abandon cargo. - // if we have a new order + // if we were ordered to gather something else, try that. if (this.ent.resourceCarrying()[0].type !== this.ent.getMetadata(PlayerID, "gather-type")) - this.startGathering(gameState); + this.startGathering(baseManager,gameState); else { + // okay so we haven't found a proper dropsite for the resource we're supposed to gather + // so let's get idle and the base manager will reassign us, hopefully well. this.ent.setMetadata(PlayerID, "gather-type",undefined); this.ent.setMetadata(PlayerID, "subrole", "idle"); this.ent.stopMoving(); @@ -45,16 +64,53 @@ Worker.prototype.update = function(gameState) { } Engine.ProfileStop(); } - this.startApproachingResourceTime = gameState.getTimeElapsed(); - + // debug: show the resource we're gathering from //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]}); } else if (this.ent.unitAIState().split(".")[1] === "GATHER") { - if (this.unsatisfactoryResource && (this.ent.id() + gameState.ai.playedTurn) % 20 === 0) + + // check for transport. + if (gameState.ai.playedTurn % 5 === 0) + { + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"]) + { + var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + if (ress !== undefined) + { + var index = gameState.ai.accessibility.getAccessValue(ress.position()); + var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position()); + if (index !== mIndex && index !== 1) + { + //gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position()); + } + } + } + } + + /* + if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) + { + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" + && this.ent.unitAIOrderData()[0]["target"]) + { + var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + debug ("here " + this.startApproachingResourceAmount + "," + ent.resourceSupplyAmount()); + if (ent && this.startApproachingResourceAmount == ent.resourceSupplyAmount() && this.startEnt == ent.id()) { + debug (ent.toString() + " is inaccessible"); + ent.setMetadata(PlayerID, "inaccessible", true); + this.ent.flee(ent); + this.ent.setMetadata(PlayerID, "subrole", "idle"); + } + } + }*/ + + // we're gathering. Let's check that it's not a resource we'd rather not gather from. + if ((this.ent.id() + gameState.ai.playedTurn) % 6 === 0 && this.checkUnsatisfactoryResource(gameState)) { Engine.ProfileStart("Start Gathering"); - this.startGathering(gameState); + this.startGathering(baseManager,gameState); Engine.ProfileStop(); } + // TODO: reimplement the "reaching time" check. /*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) { if (this.gatheringFrom) { var ent = gameState.getEntityById(this.gatheringFrom); @@ -67,7 +123,7 @@ Worker.prototype.update = function(gameState) { this.gatheringFrom = undefined; } } - }*/ + }*/ } else if (this.ent.unitAIState().split(".")[1] === "COMBAT") { /*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) { var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0].target); @@ -80,41 +136,78 @@ Worker.prototype.update = function(gameState) { this.gatheringFrom = undefined; } }*/ - } else { - this.startApproachingResourceTime = gameState.getTimeElapsed(); } } else if(subrole === "builder") { - if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){ - var target = this.ent.getMetadata(PlayerID, "target-foundation"); - if (target.foundationProgress() === undefined && target.needsRepair() == false) - this.ent.setMetadata(PlayerID, "subrole", "idle"); + + // check for transport. + if (gameState.ai.playedTurn % 5 === 0) + { + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"]) + { + var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + if (ress !== undefined) + { + var index = gameState.ai.accessibility.getAccessValue(ress.position()); + var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position()); + if (index !== mIndex && index !== 1) + { + //gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position()); + } + } + } + } + + + if (this.ent.unitAIState().split(".")[1] !== "REPAIR") { + var target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); + // okay so apparently we aren't working. + // Unless we've been explicitely told to keep our role, make us idle. + if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) + { + if (!this.ent.getMetadata(PlayerID, "keepSubrole")) + this.ent.setMetadata(PlayerID, "subrole", "idle"); + } else this.ent.repair(target); } this.startApproachingResourceTime = gameState.getTimeElapsed(); //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]}); + // TODO: we should maybe decide on our own to build other buildings, not rely on the assigntofoundation stuff. } else if(subrole === "hunter") { - if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){ + if (this.ent.isIdle()){ Engine.ProfileStart("Start Hunting"); - this.startHunting(gameState); + this.startHunting(gameState, baseManager); Engine.ProfileStop(); } - } else { - this.startApproachingResourceTime = gameState.getTimeElapsed(); } }; -Worker.prototype.startGathering = function(gameState){ +// check if our current resource is unsatisfactory +// this can happen in two ways: +// -either we were on an unsatisfactory resource last time we started gathering (this.unsatisfactoryResource) +// -Or we auto-moved to a bad resource thanks to the great UnitAI. +Worker.prototype.checkUnsatisfactoryResource = function(gameState) { + if (this.unsatisfactoryResource) + return true; + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIState().split(".")[2] === "GATHERING" && this.ent.unitAIOrderData()[0]["target"]) + { + var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + if (!ress || !ress.getMetadata(PlayerID,"linked-dropsite") || !ress.getMetadata(PlayerID,"linked-dropsite-nearby") || gameState.ai.accessibility.getAccessValue(ress.position()) === -1) + return true; + } + return false; +}; + +Worker.prototype.startGathering = function(baseManager, gameState) { var resource = this.ent.getMetadata(PlayerID, "gather-type"); var ent = this.ent; + var self = this; if (!ent.position()){ // TODO: work out what to do when entity has no position return; } - this.unsatisfactoryResource = false; - // TODO: this is not necessarily optimal. // find closest dropsite which has nearby resources of the correct type @@ -125,67 +218,77 @@ Worker.prototype.startGathering = function(gameState){ // first step: count how many dropsites we have that have enough resources "close" to them. // TODO: this is a huge part of multi-base support. Count only those in the same base as the worker. var number = 0; - var ourDropsites = gameState.getOwnDropsites(resource); + var ourDropsites = EntityCollectionFromIds(gameState,Object.keys(baseManager.dropsites)); if (ourDropsites.length === 0) { debug ("We do not have a dropsite for " + resource + ", aborting"); return; } - ourDropsites.forEach(function (dropsite) { - if (dropsite.getMetadata(PlayerID, "linked-resources-" +resource) !== undefined - && dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) !== undefined && dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 200) { - number++; - } - }); + var maxPerDP = 20; + if (resource === "food") + maxPerDP = 200; - //debug ("Available " +resource + " dropsites: " +ourDropsites.length); + ourDropsites.forEach(function (dropsite) { + if (baseManager.dropsites[dropsite.id()][resource] && baseManager.dropsites[dropsite.id()][resource][4] > 1000 + && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) + number++; + }); // Allright second step, if there are any such dropsites, we pick the closest. // we pick one with a lot of resource, or we pick the only one available (if it's high enough, otherwise we'll see with "far" below). if (number > 0) { ourDropsites.forEach(function (dropsite) { //}){ - if (dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) == undefined) + if (baseManager.dropsites[dropsite.id()][resource] === undefined) return; - if (dropsite.position() && (dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 700 || (number === 1 && dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 200) ) ) { + if (dropsite.position() && (baseManager.dropsites[dropsite.id()][resource][4] > 1000 || (number === 1 && baseManager.dropsites[dropsite.id()][resource][4] > 200) ) + && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) { var dist = SquareVectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; - nearestResources = dropsite.getMetadata(PlayerID, "linked-resources-" + resource); + nearestResources = baseManager.dropsites[dropsite.id()][resource][1]; nearestDropsite = dropsite; } } }); } - //debug ("Nearest dropsite: " +nearestDropsite); - - // Now if we have no dropsites, we repeat the process with resources "far" from dropsites but still linked with them. - // I add the "close" value for code sanity. - // Again, we choose a dropsite with a lot of resources left, or we pick the only one available (in this case whatever happens). + // we've found no fitting dropsites close enough from us. + // So'll try with far away. if (!nearestResources || nearestResources.length === 0) { - //debug ("here(1)"); - gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){ - var quantity = dropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+dropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource); - if (dropsite.position() && (quantity) > 700 || number === 1) { + ourDropsites.forEach(function (dropsite) { //}){ + if (baseManager.dropsites[dropsite.id()][resource] === undefined) + return; + if (dropsite.position() && baseManager.dropsites[dropsite.id()][resource][4] > 400 + && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) { var dist = SquareVectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; - nearestResources = dropsite.getMetadata(PlayerID, "linked-resources-" + resource); + nearestResources = baseManager.dropsites[dropsite.id()][resource][1]; nearestDropsite = dropsite; } } }); - this.unsatisfactoryResource = true; - //debug ("Nearest dropsite: " +nearestDropsite); } - // If we still haven't found any fitting dropsite... - // Then we'll just pick any resource, and we'll check for the closest dropsite to that one + if (!nearestResources || nearestResources.length === 0){ + if (resource === "food") + if (this.buildAnyField(gameState)) + return; + + if (this.unsatisfactoryResource == true) + return; // we were already not satisfied, we're still not, change not. + + if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) + return; + //debug ("No fitting dropsite for " + resource + " found, iterating the map."); nearestResources = gameState.getResourceSupplies(resource); this.unsatisfactoryResource = true; + // TODO: should try setting up dropsites. + } else { + this.unsatisfactoryResource = false; } if (nearestResources.length === 0){ @@ -193,10 +296,16 @@ Worker.prototype.startGathering = function(gameState){ { if (this.buildAnyField(gameState)) return; + if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) + return; debug("No " + resource + " found! (1)"); } else + { + if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) + return; debug("No " + resource + " found! (1)"); + } return; } //debug("Found " + nearestResources.length + "spots for " + resource); @@ -213,7 +322,6 @@ Worker.prototype.startGathering = function(gameState){ // filter resources // TODo: add a bonus for resources with a lot of resources left, perhaps, to spread gathering? nearestResources.forEach(function(supply) { //}){ - // sanity check, perhaps sheep could be garrisoned? if (!supply.position()) { //debug ("noposition"); @@ -224,28 +332,28 @@ Worker.prototype.startGathering = function(gameState){ //debug ("inaccessible"); return; } - - if (supply.isFull() === true || (supply.maxGatherers() - supply.resourceSupplyGatherers().length == 0) || - (gameState.turnCache["ressGathererNB"] && gameState.turnCache["ressGathererNB"][supply.id()] - && gameState.turnCache["ressGathererNB"][supply.id()] + supply.resourceSupplyGatherers().length >= supply.maxGatherers())) { + + if (supply.isFull() === true + || (gameState.turnCache["ressGathererNB"] && gameState.turnCache["ressGathererNB"][supply.id()] + && gameState.turnCache["ressGathererNB"][supply.id()] + supply.resourceSupplyGatherers().length >= supply.maxGatherers)) return; - } + - // Don't gather enemy farms - if (!supply.isOwn(PlayerID) && supply.owner() !== 0) { + // Don't gather enemy farms or farms from another base + if ((!supply.isOwn(PlayerID) && supply.owner() !== 0) || (supply.isOwn(PlayerID) && supply.getMetadata(PlayerID,"base") !== self.baseID)) { //debug ("enemy"); return; } // quickscope accessbility check. - if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(), true)) { + if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position())) { //debug ("nopath"); return; } // some simple check for chickens: if they're in a square that's inaccessible, we won't gather from them. if (supply.footprintRadius() < 1) { - var fakeMap = new Map(gameState,gameState.getMap().data); + var fakeMap = new Map(gameState.sharedScript,gameState.getMap().data); var id = fakeMap.gamePosToMapPos(supply.position())[0] + fakeMap.width*fakeMap.gamePosToMapPos(supply.position())[1]; if ( (gameState.sharedScript.passabilityClasses["pathfinderObstruction"] & gameState.getMap().data[id]) ) { @@ -268,12 +376,10 @@ Worker.prototype.startGathering = function(gameState){ var territoryOwner = Map.createTerritoryMap(gameState).getOwner(supply.position()); if (territoryOwner != PlayerID && territoryOwner != 0) { - dist *= 3.0; + dist *= 5.0; //return; - } - - // Go for treasure as a priority - if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){ + } else if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){ + // go for treasures if they're not in enemy territory dist /= 1000; } @@ -282,7 +388,6 @@ Worker.prototype.startGathering = function(gameState){ nearestSupply = supply; } }); - if (nearestSupply) { var pos = nearestSupply.position(); @@ -310,11 +415,11 @@ Worker.prototype.startGathering = function(gameState){ { tried = this.buildAnyField(gameState); if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) { + // TODO: ought to change behavior here. return; // wait. a farm should appear. } } if (!tried) { - if (!gameState.turnCache["ressGathererNB"]) { gameState.turnCache["ressGathererNB"] = {}; @@ -324,41 +429,28 @@ Worker.prototype.startGathering = function(gameState){ else gameState.turnCache["ressGathererNB"][nearestSupply.id()]++; - this.maxApproachTime = Math.max(25000, VectorDistance(pos,this.ent.position()) * 1000); + this.maxApproachTime = Math.max(30000, VectorDistance(pos,this.ent.position()) * 5000); + this.startApproachingResourceAmount = ent.resourceSupplyAmount(); + this.startEnt = ent.id(); ent.gather(nearestSupply); ent.setMetadata(PlayerID, "target-foundation", undefined); - - // check if the resource we've started gathering from is now full, in which case inform the dropsite. - if (gameState.turnCache["ressGathererNB"][nearestSupply.id()] + nearestSupply.resourceSupplyGatherers().length >= nearestSupply.maxGatherers() - && nearestSupply.getMetadata(PlayerID, "linked-dropsite") != undefined) - { - var dropsite = gameState.getEntityById(nearestSupply.getMetadata(PlayerID, "linked-dropsite")); - if (dropsite == undefined || dropsite.getMetadata(PlayerID, "linked-resources-" + resource) === undefined) - return; - if (nearestSupply.getMetadata(PlayerID, "linked-dropsite-nearby") == true) { - dropsite.setMetadata(PlayerID, "resource-quantity-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) - (+nearestSupply.getMetadata(PlayerID, "dp-update-value"))); - dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(nearestSupply); - dropsite.getMetadata(PlayerID, "nearby-resources-" + resource).updateEnt(nearestSupply); - } else { - dropsite.setMetadata(PlayerID, "resource-quantity-far-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) - (+nearestSupply.getMetadata(PlayerID, "dp-update-value"))); - dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(nearestSupply); - } - - } - } } else { if (resource === "food" && this.buildAnyField(gameState)) return; - debug("No " + resource + " found! (2)"); + if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) + return; + + if (resource !== "food") + debug("No " + resource + " found! (2)"); // If we had a fitting closest dropsite with a lot of resources, mark it as not good. It means it's probably full. Then retry. // it'll be resetted next time it's counted anyway. if (nearestDropsite && nearestDropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+nearestDropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource) > 400) { nearestDropsite.setMetadata(PlayerID, "resource-quantity-" +resource, 0); nearestDropsite.setMetadata(PlayerID, "resource-quantity-far-" +resource, 0); - this.startGathering(gameState); + this.startGathering(baseManager, gameState); } } }; @@ -397,10 +489,10 @@ Worker.prototype.returnResources = function(gameState){ return true; }; -Worker.prototype.startHunting = function(gameState){ +Worker.prototype.startHunting = function(gameState, baseManager){ var ent = this.ent; - if (!ent.position() || ent.getMetadata(PlayerID, "stoppedHunting")) + if (!ent.position() || !baseManager.isHunting) return; // So here we're doing it basic. We check what we can hunt, we hunt it. No fancies. @@ -438,7 +530,7 @@ Worker.prototype.startHunting = function(gameState){ } // quickscope accessbility check - if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(), true)) + if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(),false, true)) return; if (dist < nearestSupplyDist) { @@ -464,20 +556,20 @@ Worker.prototype.startHunting = function(gameState){ }); if (!nearestDropsite) { - ent.setMetadata(PlayerID, "stoppedHunting", true); + baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); debug ("No dropsite for hunting food"); return; } - if (minDropsiteDist > 45000) { - ent.setMetadata(PlayerID, "stoppedHunting", true); + if (minDropsiteDist > 35000) { + baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); } else { ent.gather(nearestSupply); ent.setMetadata(PlayerID, "target-foundation", undefined); } } else { - ent.setMetadata(PlayerID, "stoppedHunting", true); + baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); debug("No food found for hunting! (2)"); } @@ -495,10 +587,32 @@ Worker.prototype.getResourceType = function(type){ } }; +Worker.prototype.getGatherRate = function(gameState) { + if (this.ent.getMetadata(PlayerID,"subrole") !== "gatherer") + return 0; + var rates = this.ent.resourceGatherRates(); + + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0]["target"]) + { + var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + if (!ress) + return 0; + var type = ress.resourceSupplyType(); + if (type.generic == "treasure") + return 1000; + var tstring = type.generic + "." + type.specific; + //debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName); + if (rates[tstring]) + return rates[tstring]; + return 0; + } + return 0; +}; + Worker.prototype.buildAnyField = function(gameState){ var self = this; var okay = false; - var foundations = gameState.getOwnFoundations(); + var foundations = gameState.getOwnFoundations().filter(Filters.byMetadata(PlayerID,"base",this.baseID)); foundations.filterNearest(this.ent.position(), foundations.length); foundations.forEach(function (found) { if (found._template.BuildRestrictions.Category === "Field" && !okay) { @@ -507,5 +621,18 @@ Worker.prototype.buildAnyField = function(gameState){ return; } }); + if (!okay) + { + var foundations = gameState.getOwnFoundations(); + foundations.filterNearest(this.ent.position(), foundations.length); + foundations.forEach(function (found) { + if (found._template.BuildRestrictions.Category === "Field" && !okay) { + self.ent.repair(found); + self.ent.setMetadata(PlayerID,"base", found.getMetadata(PlayerID,"base")); + okay = true; + return; + } + }); + } return okay; }; diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/base.js b/binaries/data/mods/public/simulation/ai/common-api-v3/base.js index ad87573317..608bcac78b 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/base.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/base.js @@ -12,6 +12,7 @@ function BaseAI(settings) PlayerID = this._player; this.turn = 0; + this.timeElapsed = 0; } //Return a simple object (using no classes etc) that will be serialized @@ -35,13 +36,14 @@ BaseAI.prototype.InitWithSharedScript = function(state, sharedAI) this.terrainAnalyzer = sharedAI.terrainAnalyzer; this.passabilityClasses = sharedAI.passabilityClasses; this.passabilityMap = sharedAI.passabilityMap; + this.territoryMap = sharedAI.territoryMap; + this.timeElapsed = state.timeElapsed; - var gameState = sharedAI.gameState[PlayerID]; - gameState.ai = this; + this.gameState = sharedAI.gameState[PlayerID]; + this.gameState.ai = this; + this.sharedScript = sharedAI; - this.InitShared(gameState, sharedAI); - - delete gameState.ai; + this.InitShared(this.gameState, this.sharedScript); } BaseAI.prototype.HandleMessage = function(state, sharedAI) @@ -59,7 +61,7 @@ BaseAI.prototype.HandleMessage = function(state, sharedAI) this.timeElapsed = sharedAI.timeElapsed; this.accessibility = sharedAI.accessibility; this.terrainAnalyzer = sharedAI.terrainAnalyzer; - this.techModifications = sharedAI.techModifications[this._player]; + this.techModifications = sharedAI._techModifications[this._player]; Engine.ProfileStop(); diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js b/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js index 775b92254d..59c28d782f 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js @@ -32,7 +32,14 @@ var EntityTemplate = Class({ return undefined; return this._template.Identity.RequiredTechnology; }, - + + available: function(gameState) { + if (!this._template.Identity || !this._template.Identity.RequiredTechnology) + return true; + return gameState.isResearched(this._template.Identity.RequiredTechnology); + }, + + // specifically phase: function() { if (!this._template.Identity || !this._template.Identity.RequiredTechnology) return 0; @@ -412,7 +419,25 @@ var EntityTemplate = Class({ var territories = this.buildTerritories(); return (territories && territories.indexOf(territory) != -1); }, - + + hasTerritoryInfluence: function() { + return (this._template.TerritoryInfluence !== undefined); + }, + + territoryInfluenceRadius: function() { + if (this._template.TerritoryInfluence !== undefined) + return (this._template.TerritoryInfluence.Radius); + else + return -1; + }, + + territoryInfluenceWeight: function() { + if (this._template.TerritoryInfluence !== undefined) + return (this._template.TerritoryInfluence.Weight); + else + return -1; + }, + visionRange: function() { if (!this._template.Vision) return undefined; @@ -427,7 +452,7 @@ var Entity = Class({ _init: function(sharedAI, entity) { - this._super.call(this, sharedAI.GetTemplate(entity.template), sharedAI.techModifications[entity.owner]); + this._super.call(this, sharedAI.GetTemplate(entity.template), sharedAI._techModifications[entity.owner]); this._ai = sharedAI; this._templateName = entity.template; @@ -462,9 +487,13 @@ var Entity = Class({ this._ai.setMetadata(player, this, key, value); }, - deleteMetadata: function(player) { + deleteAllMetadata: function(player) { delete this._ai._entityMetadata[player][this.id()]; }, + + deleteMetadata: function(player, key) { + this._ai.deleteMetadata(player, this, key); + }, position: function() { return this._entity.position; }, @@ -500,7 +529,7 @@ var Entity = Class({ }, foundationProgress: function() { - if (typeof this._entity.foundationProgress === "undefined") + if (this._entity.foundationProgress == undefined) return undefined; return this._entity.foundationProgress; }, @@ -537,6 +566,7 @@ var Entity = Class({ { if (this._entity.resourceSupplyGatherers !== undefined) return (this.maxGatherers() === this._entity.resourceSupplyGatherers.length); + return undefined; }, @@ -547,6 +577,8 @@ var Entity = Class({ }, garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); }, + + canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, // TODO: visibility @@ -658,7 +690,7 @@ var Entity = Class({ return this; }, - construct: function(template, x, z, angle) { + construct: function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting @@ -671,7 +703,8 @@ var Entity = Class({ "angle": angle, "autorepair": false, "autocontinue": false, - "queued": false + "queued": false, + "metadata" : metadata // can be undefined }); return this; }, diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js b/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js index 02a65d8ffe..3f4dada5ff 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js @@ -3,6 +3,9 @@ function EntityCollection(sharedAI, entities, filters) this._ai = sharedAI; this._entities = entities || {}; this._filters = filters || []; + + this._quickIter = false; // will make the entity collection store an array (not associative) of entities used when calling "foreach". + // probably should not be usde for very dynamic entity collections. // Compute length lazily on demand, since it can be // expensive for large collections @@ -23,6 +26,8 @@ function EntityCollection(sharedAI, entities, filters) // If an entitycollection is frozen, it will never automatically add a unit. // But can remove one. +// this makes it easy to create entity collection that will auto-remove dead units +// but never add new ones. EntityCollection.prototype.freeze = function() { this.frozen = true; @@ -32,6 +37,14 @@ EntityCollection.prototype.defreeze = function() this.frozen = false; }; +EntityCollection.prototype.allowQuickIter = function() +{ + this._quickIter = true; + this._entitiesArray = []; + for each (var ent in this._entities) + this._entitiesArray.push(ent); +}; + EntityCollection.prototype.toIdArray = function() { var ret = []; @@ -42,6 +55,8 @@ EntityCollection.prototype.toIdArray = function() EntityCollection.prototype.toEntityArray = function() { + if (this._quickIter === true) + return this._entitiesArray; var ret = []; for each (var ent in this._entities) ret.push(ent); @@ -59,12 +74,23 @@ EntityCollection.prototype.toString = function() EntityCollection.prototype.filterNearest = function(targetPos, n) { // Compute the distance of each entity - var data = []; // [ [id, ent, distance], ... ] - for (var id in this._entities) + var data = []; // [id, ent, distance] + + if (this._quickIter === true) { - var ent = this._entities[id]; - if (ent.position()) - data.push([id, ent, VectorDistance(targetPos, ent.position())]); + for (var i in this._entitiesArray) + { + var ent = this._entitiesArray[i]; + if (ent.position() !== -1) + data.push([ent.id(), ent, SquareVectorDistance(targetPos, ent.position())]); + } + } else { + for (var id in this._entities) + { + var ent = this._entities[id]; + if (ent.position() !== -1) + data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); + } } // Sort by increasing distance @@ -72,8 +98,9 @@ EntityCollection.prototype.filterNearest = function(targetPos, n) // Extract the first n var ret = {}; - for each (var val in data.slice(0, n)) - ret[val[0]] = val[1]; + var length = Math.min(n, entData.length); + for (var i = 0; i < length; ++i) + ret[data[i][0]] = data[i][1]; return new EntityCollection(this._ai, ret); }; @@ -84,11 +111,22 @@ EntityCollection.prototype.filter = function(filter, thisp) filter = {"func": filter, "dynamicProperties": []}; var ret = {}; - for (var id in this._entities) + if (this._quickIter === true) { - var ent = this._entities[id]; - if (filter.func.call(thisp, ent, id, this)) - ret[id] = ent; + for (var i in this._entitiesArray) + { + var ent = this._entitiesArray[i]; + var id = ent.id(); + if (filter.func.call(thisp, ent, id, this)) + ret[id] = ent; + } + } else { + for (var id in this._entities) + { + var ent = this._entities[id]; + if (filter.func.call(thisp, ent, id, this)) + ret[id] = ent; + } } return new EntityCollection(this._ai, ret, this._filters.concat([filter])); @@ -107,6 +145,23 @@ EntityCollection.prototype.filter_raw = function(callback, thisp) return new EntityCollection(this._ai, ret); }; +EntityCollection.prototype.forEach = function(callback) +{ + if (this._quickIter === true) + { + for (var id in this._entitiesArray) + { + callback(this._entitiesArray[id]); + } + return this; + } + for (var id in this._entities) + { + callback(this._entities[id]); + } + return this; +}; + EntityCollection.prototype.filterNearest = function(targetPos, n) { // Compute the distance of each entity @@ -130,16 +185,6 @@ EntityCollection.prototype.filterNearest = function(targetPos, n) return new EntityCollection(this._ai, ret); }; -EntityCollection.prototype.forEach = function(callback, thisp) -{ - for (var id in this._entities) - { - var ent = this._entities[id]; - callback.call(thisp, ent, id, this); - } - return this; -}; - EntityCollection.prototype.move = function(x, z, queued) { queued = queued || false; @@ -241,6 +286,8 @@ EntityCollection.prototype.removeEnt = function(ent) // Checking length may initialize it, so do it before deleting. if (this.length !== undefined) this._length--; + if (this._quickIter === true) + this._entitiesArray.splice(this._entitiesArray.indexOf(ent),1); delete this._entities[ent.id()]; return true; } @@ -263,6 +310,8 @@ EntityCollection.prototype.addEnt = function(ent) if (this.length !== undefined) this._length++; this._entities[ent.id()] = ent; + if (this._quickIter === true) + this._entitiesArray.push(ent); return true; } }; @@ -297,6 +346,11 @@ EntityCollection.prototype.registerUpdates = function(noPush) this._ai.registerUpdatingEntityCollection(this,noPush); }; +EntityCollection.prototype.unregister = function() +{ + this._ai.removeUpdatingEntityCollection(this); +}; + EntityCollection.prototype.dynamicProperties = function() { var ret = []; diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js b/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js index 5b14fb6d50..2787dc11cd 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js @@ -41,6 +41,15 @@ var Filters = { }, "dynamicProperties": ['metadata.' + key]}; }, + + // can be used for stuffs which won't change once entities are created. + byStaticMetadata: function(player, key, value){ + return {"func" : function(ent){ + return (ent.getMetadata(player, key) == value); + }, + "dynamicProperties": []}; + }, + byHasMetadata: function(player, key){ return {"func" : function(ent){ return (ent.getMetadata(player, key) != undefined); @@ -97,7 +106,7 @@ var Filters = { byCanGarrison: function(){ return {"func" : function(ent){ - return ent.garrisonMax(); + return ent.garrisonMax() > 0; }, "dynamicProperties": []}; }, @@ -126,6 +135,13 @@ var Filters = { }, "dynamicProperties": []}; }, + + isGarrisoned: function(){ + return {"func" : function(ent){ + return ent.position() == -1; // assumes garrisoned + }, + "dynamicProperties": []}; + }, isSoldier: function(){ return {"func" : function(ent){ @@ -184,7 +200,7 @@ var Filters = { isDropsite: function(resourceType){ return {"func": function(ent){ - return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1); + return (ent.resourceDropsiteTypes() && (resourceType === undefined || ent.resourceDropsiteTypes().indexOf(resourceType) !== -1)); }, "dynamicProperties": []}; }, @@ -203,7 +219,7 @@ var Filters = { return false; // And don't go for the bloody fish! TODO: better accessibility checks - if (ent.hasClass("SeaCreature")){ + if (ent.hasClass("SeaCreature")) { return false; } diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js b/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js index 498a2ef3a2..03628a01da 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js @@ -2,51 +2,56 @@ * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ -var GameState = function(SharedScript, state, player) { +var GameState = function() { + this.ai = null; // must be updated by the AIs. + this.cellSize = 4; // Size of each map tile + + this.buildingsBuilt = 0; + this.turnCache = {}; +}; + +GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; - this.timeElapsed = state.timeElapsed; + this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; - this.playerData = state.players[player]; - this.techModifications = SharedScript.techModifications[player]; - this.buildingsBuilt = 0; - - this.ai = null; // must be updated by the AIs. - - this.cellSize = 4; // Size of each map tile - - this.turnCache = {}; + this.playerData = this.sharedScript.playersData[this.player]; + this.techModifications = SharedScript._techModifications[this.player]; }; + GameState.prototype.update = function(SharedScript, state) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; - this.timeElapsed = state.timeElapsed; + this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; + this._entities = SharedScript._entities; this.entities = SharedScript.entities; - this.playerData = state.players[this.player]; - this.techModifications = SharedScript.techModifications[this.player]; + this.playerData = SharedScript.playersData[this.player]; + this.techModifications = SharedScript._techModifications[this.player]; this.buildingsBuilt = 0; this.turnCache = {}; }; -GameState.prototype.updatingCollection = function(id, filter, collection){ +GameState.prototype.updatingCollection = function(id, filter, collection, allowQuick){ // automatically add the player ID id = this.player + "-" + id; - if (!this.EntCollecNames[id]){ if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else { this.EntCollecNames[id] = this.entities.filter(filter); } + if (allowQuick) + this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); + // warn ("New Collection named " +id); } return this.EntCollecNames[id]; @@ -69,13 +74,16 @@ GameState.prototype.getEC = function(id){ return undefined; }; -GameState.prototype.updatingGlobalCollection = function(id, filter, collection) { +GameState.prototype.updatingGlobalCollection = function(id, filter, collection, allowQuick) { if (!this.EntCollecNames[id]){ if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else this.EntCollecNames[id] = this.entities.filter(filter); + if (allowQuick) + this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); + //warn ("New Global Collection named " +id); } return this.EntCollecNames[id]; @@ -105,6 +113,18 @@ GameState.prototype.currentPhase = function() return 0; }; +GameState.prototype.townPhase = function() +{ + if (this.playerData.civ == "athen") + return "phase_town_athen"; + return "phase_town_generic"; +}; + +GameState.prototype.cityPhase = function() +{ + return "phase_city_generic"; +}; + GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs[template] !== undefined; @@ -228,6 +248,7 @@ GameState.prototype.getTemplate = function(type) GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; + GameState.prototype.civ = function() { return this.playerData.civ; }; @@ -349,48 +370,56 @@ GameState.prototype.getEntityById = function(id){ return undefined; }; -GameState.prototype.getOwnEntitiesByMetadata = function(key, value){ - return this.updatingCollection(key + "-" + value, Filters.byMetadata(this.player, key, value),this.getOwnEntities()); +GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain){ + if (maintain === true) + return this.updatingCollection(key + "-" + value, Filters.byMetadata(this.player, key, value),this.getOwnEntities()); + return this.getOwnEntities().filter(Filters.byMetadata(this.player, key, value)); }; GameState.prototype.getOwnEntitiesByRole = function(role){ - return this.getOwnEntitiesByMetadata("role", role); + return this.getOwnEntitiesByMetadata("role", role, true); }; GameState.prototype.getOwnTrainingFacilities = function(){ - return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities()); + return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities(), true); }; GameState.prototype.getOwnResearchFacilities = function(){ - return this.updatingCollection("own-research-facilities", Filters.byResearchAvailable(), this.getOwnEntities()); + return this.updatingCollection("own-research-facilities", Filters.byResearchAvailable(), this.getOwnEntities(), true); }; -GameState.prototype.getOwnEntitiesByType = function(type){ +GameState.prototype.getOwnEntitiesByType = function(type, maintain){ var filter = Filters.byType(type); - return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities()); + if (maintain === true) + return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities()); + return this.getOwnEntities().filter(filter); + }; -GameState.prototype.countEntitiesByType = function(type) { - return this.getOwnEntitiesByType(type).length; +GameState.prototype.countEntitiesByType = function(type, maintain) { + return this.getOwnEntitiesByType(type, maintain).length; }; GameState.prototype.countEntitiesAndQueuedByType = function(type) { - var count = this.countEntitiesByType(type); + var count = this.countEntitiesByType(type, true); // Count building foundations - count += this.countEntitiesByType("foundation|" + type); - - // Count animal resources - count += this.countEntitiesByType("resource|" + type); - - // Count entities in building production queues - this.getOwnTrainingFacilities().forEach(function(ent){ - ent.trainingQueue().forEach(function(item) { - if (item.unitTemplate == type){ - count += item.count; - } + if (this.getTemplate(type).hasClass("Structure") === true) + count += this.countEntitiesByType("foundation|" + type, true); + else if (this.getTemplate(type).resourceSupplyType() !== undefined) // animal resources + count += this.countEntitiesByType("resource|" + type, true); + else + { + // Count entities in building production queues + // TODO: maybe this fails for corrals. + this.getOwnTrainingFacilities().forEach(function(ent){ + ent.trainingQueue().forEach(function(item) { + if (item.unitTemplate == type){ + count += item.count; + } + }); }); - }); + } return count; }; @@ -508,11 +537,13 @@ GameState.prototype.getOwnFoundations = function() { }; GameState.prototype.getOwnDropsites = function(resource){ - return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities()); + if (resource !== undefined) + return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities(), true); + return this.updatingCollection("dropsite-own", Filters.isDropsite(), this.getOwnEntities(), true); }; GameState.prototype.getResourceSupplies = function(resource){ - return this.updatingGlobalCollection("resource-" + resource, Filters.byResource(resource), this.getEntities()); + return this.updatingGlobalCollection("resource-" + resource, Filters.byResource(resource), this.getEntities(), true); }; GameState.prototype.getEntityLimits = function() { @@ -589,8 +620,7 @@ GameState.prototype.findAvailableTech = function() { if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else { - if (this.canResearch(allResearchable[i]) && template._templateName != "phase_town_generic" - && template._templateName != "phase_town_athens" && template._templateName != "phase_city_generic") + if (this.canResearch(allResearchable[i]) && template._templateName != this.townPhase() && template._templateName != this.cityPhase()) ret.push( [allResearchable[i], template] ); } } diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js b/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js index 2f571c3cd1..720e89b435 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js @@ -2,6 +2,8 @@ * Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values. */ +const TERRITORY_PLAYER_MASK = 0x3F; + function Map(sharedScript, originalMap, actualCopy){ // get the map to find out the correct dimensions var gameMap = sharedScript.passabilityMap; @@ -9,6 +11,8 @@ function Map(sharedScript, originalMap, actualCopy){ this.height = gameMap.height; this.length = gameMap.data.length; + this.maxVal = 255; + if (originalMap && actualCopy){ this.map = new Uint8Array(this.length); for (var i = 0; i < originalMap.length; ++i) @@ -21,8 +25,12 @@ function Map(sharedScript, originalMap, actualCopy){ this.cellSize = 4; } +Map.prototype.setMaxVal = function(val){ + this.maxVal = val; +}; + Map.prototype.gamePosToMapPos = function(p){ - return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)]; + return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)]; }; Map.prototype.point = function(p){ @@ -43,13 +51,13 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { switch (type){ case 'linear': str = +strength / +maxDist; - break; + break; case 'quadratic': str = +strength / +maxDist2; - break; + break; case 'constant': str = +strength; - break; + break; } for ( var y = y0; y < y1; ++y) { @@ -71,13 +79,12 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { quant = str; break; } - if (this.map[x + y * this.width] + quant > 255){ - this.map[x + y * this.width] = 255; - } else if (this.map[x + y * this.width] + quant < 0){ + if (this.map[x + y * this.width] + quant < 0) this.map[x + y * this.width] = 0; - } else { + else if (this.map[x + y * this.width] + quant > this.maxVal) + this.map[x + y * this.width] = this.maxVal; // avoids overflow. + else this.map[x + y * this.width] += quant; - } } } } @@ -126,15 +133,18 @@ Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) { break; } var machin = this.map[x + y * this.width] * quant; - if (machin <= 0){ - this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0 - }else{ + if (machin < 0) + this.map[x + y * this.width] = 0; + else if (machin > this.maxVal) + this.map[x + y * this.width] = this.maxVal; + else this.map[x + y * this.width] = machin; - } } } } }; + +// doesn't check for overflow. Map.prototype.setInfluence = function(cx, cy, maxDist, value) { value = value ? value : 0; @@ -178,8 +188,9 @@ Map.prototype.sumInfluence = function(cx, cy, radius){ return sum; }; /** - * Make each cell's 8-bit value at least one greater than each of its - * neighbours' values. Possible assignment of a cap (maximum). + * Make each cell's 16-bit/8-bit value at least one greater than each of its + * neighbours' values. (If the grid is initialised with 0s and 65535s or 255s, the + * result of each cell is its Manhattan distance to the nearest 0.) */ Map.prototype.expandInfluences = function(maximum, map) { var grid = this.map; @@ -187,7 +198,7 @@ Map.prototype.expandInfluences = function(maximum, map) { grid = map; if (maximum == undefined) - maximum = 255; + maximum = this.maxVal; var w = this.width; var h = this.height; for ( var y = 0; y < h; ++y) { @@ -266,7 +277,18 @@ Map.prototype.multiply = function(map, onlyBetter, divider, maxMultiplier){ }; // add to current map by the parameter map pixelwise Map.prototype.add = function(map){ - for (var i = 0; i < this.length; ++i){ - this.map[i] += +map.map[i]; + for (var i = 0; i < this.length; ++i) { + if (this.map[i] + map.map[i] < 0) + this.map[i] = 0; + else if (this.map[i] + map.map[i] > this.maxVal) + this.map[i] = this.maxVal; + else + this.map[i] += map.map[i]; } }; + +Map.prototype.dumpIm = function(name, threshold){ + name = name ? name : "default.png"; + threshold = threshold ? threshold : this.maxVal; + Engine.DumpImage(name, this.map, this.width, this.height, threshold); +}; diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js b/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js index 3989704880..c60e779ff7 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js @@ -22,7 +22,15 @@ function SharedScript(settings) this._entityCollectionsName = {}; this._entityCollectionsByDynProp = {}; this._entityCollectionsUID = 0; - + + // A few notes about these maps. They're updated by checking for "create" and "destroy" events for all resources + // TODO: change the map when the resource amounts change for at least stone and metal mines. + this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal + this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. + // Resource maps data. + // By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4"). + this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; + this.turn = 0; } @@ -160,31 +168,44 @@ SharedScript.prototype.GetTemplate = function(name) // initialize the shared component using a given gamestate (the initial gamestate after map creation, usually) // this is called right at the end of map generation, before you actually reach the map. SharedScript.prototype.initWithState = function(state) { + this.events = state.events; this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; + this.players = this._players; + this.playersData = state.players; this.territoryMap = state.territoryMap; + this.timeElapsed = state.timeElapsed; for (var o in state.players) this._techModifications[o] = state.players[o].techModifications; - this.techModifications = this._techModifications; - this._entities = {}; - for (var id in state.entities) + this.entities = new EntityCollection(this); + + /* (var id in state.entities) { this._entities[id] = new Entity(this, state.entities[id]); } // entity collection updated on create/destroy event. this.entities = new EntityCollection(this, this._entities); + */ + this.ApplyEntitiesDelta(state); + // create the terrain analyzer - this.terrainAnalyzer = new TerrainAnalysis(this, state); - this.accessibility = new Accessibility(state, this.terrainAnalyzer); + this.terrainAnalyzer = new TerrainAnalysis(); + this.terrainAnalyzer.init(this, state); + this.accessibility = new Accessibility(); + this.accessibility.init(state, this.terrainAnalyzer); + // defined in TerrainAnalysis.js + this.updateResourceMaps(this, this.events); + this.gameState = {}; for (var i in this._players) { - this.gameState[this._players[i]] = new GameState(this,state,this._players[i]); + this.gameState[this._players[i]] = new GameState(); + this.gameState[this._players[i]].init(this,state,this._players[i]); } }; @@ -193,8 +214,9 @@ SharedScript.prototype.initWithState = function(state) { // applies entity deltas, and each gamestate. SharedScript.prototype.onUpdate = function(state) { - this.ApplyEntitiesDelta(state); - + if (this.turn !== 0) + this.ApplyEntitiesDelta(state); + Engine.ProfileStart("onUpdate"); this.events = state.events; @@ -211,6 +233,8 @@ SharedScript.prototype.onUpdate = function(state) for (var i in this.gameState) this.gameState[i].update(this,state); + if (this.turn !== 0) + this.updateResourceMaps(this, this.events); this.terrainAnalyzer.updateMapWithEvents(this); //this.OnUpdate(); @@ -223,6 +247,8 @@ SharedScript.prototype.onUpdate = function(state) SharedScript.prototype.ApplyEntitiesDelta = function(state) { Engine.ProfileStart("Shared ApplyEntitiesDelta"); + + var foundationFinished = {}; for each (var evt in state.events) { @@ -236,11 +262,10 @@ SharedScript.prototype.ApplyEntitiesDelta = function(state) this.entities.addEnt(this._entities[evt.msg.entity]); // Update all the entity collections since the create operation affects static properties as well as dynamic - for each (var entCollection in this._entityCollections) + for (var entCollection in this._entityCollections) { - entCollection.updateEnt(this._entities[evt.msg.entity]); + this._entityCollections[entCollection].updateEnt(this._entities[evt.msg.entity]); } - } else if (evt.type == "Destroy") { @@ -252,6 +277,9 @@ SharedScript.prototype.ApplyEntitiesDelta = function(state) if (!this._entities[evt.msg.entity]) continue; + if (foundationFinished[evt.msg.entity]) + evt.msg["SuccessfulFoundation"] = true; + // The entity was destroyed but its data may still be useful, so // remember the entity and this AI's metadata concerning it evt.msg.metadata = {}; @@ -280,15 +308,41 @@ SharedScript.prototype.ApplyEntitiesDelta = function(state) } } } + else if (evt.type == "ConstructionFinished") + { + // we can rely on this being before the "Destroy" command as this is the order defined by FOundation.js + // we'll move metadata. + if (!this._entities[evt.msg.entity]) + continue; + var ent = this._entities[evt.msg.entity]; + var newEnt = this._entities[evt.msg.newentity]; + if (this._entityMetadata[ent.owner()] && this._entityMetadata[ent.owner()][evt.msg.entity] !== undefined) + for (var key in this._entityMetadata[ent.owner()][evt.msg.entity]) + { + this.setMetadata(ent.owner(), newEnt, key, this._entityMetadata[ent.owner()][evt.msg.entity][key]) + } + foundationFinished[evt.msg.entity] = true; + } + else if (evt.type == "AIMetadata") + { + if (!this._entities[evt.msg.id]) + continue; // might happen in some rare cases of foundations getting destroyed, perhaps. + // Apply metadata (here for buildings for example) + for (var key in evt.msg.metadata) + { + this.setMetadata(evt.msg.owner, this._entities[evt.msg.id], key, evt.msg.metadata[key]) + } + } } for (var id in state.entities) { var changes = state.entities[id]; - + for (var prop in changes) { this._entities[id]._entity[prop] = changes[prop]; + this.updateEntityCollections(prop, this._entities[id]); } } @@ -335,9 +389,9 @@ SharedScript.prototype.updateEntityCollections = function(property, ent) { if (this._entityCollectionsByDynProp[property] !== undefined) { - for each (var entCollection in this._entityCollectionsByDynProp[property]) + for (var entCollectionid in this._entityCollectionsByDynProp[property]) { - entCollection.updateEnt(ent); + this._entityCollectionsByDynProp[property][entCollectionid].updateEnt(ent); } } } @@ -360,6 +414,16 @@ SharedScript.prototype.getMetadata = function(player, ent, key) return undefined; return metadata[key]; }; +SharedScript.prototype.deleteMetadata = function(player, ent, key) +{ + var metadata = this._entityMetadata[player][ent.id()]; + + if (!metadata || !(key in metadata)) + return true; + metadata[key] = undefined; + delete metadata[key]; + return true; +}; function copyPrototype(descendant, parent) { var sConstructor = parent.toString(); diff --git a/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js index 2b868ab144..3813a283a7 100644 --- a/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js +++ b/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js @@ -12,12 +12,16 @@ * But truly separating optimizes. */ -function TerrainAnalysis(sharedScript,rawState){ - var self = this; +function TerrainAnalysis() { this.cellSize = 4; +} + +copyPrototype(TerrainAnalysis, Map); + +TerrainAnalysis.prototype.init = function(sharedScript,rawState) { + var self = this; var passabilityMap = rawState.passabilityMap; - this.width = passabilityMap.width; this.height = passabilityMap.height; @@ -98,10 +102,11 @@ function TerrainAnalysis(sharedScript,rawState){ this.obstructionMaskLand = null; this.obstructionMaskWater = null; this.obstructionMask = null; + delete this.obstructionMaskLand; + delete this.obstructionMaskWater; + delete this.obstructionMask; }; -copyPrototype(TerrainAnalysis, Map); - // Returns the (approximately) closest point which is passable by searching in a spiral pattern TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, onLand, limitDistance, quickscope){ var w = this.width; @@ -238,34 +243,102 @@ TerrainAnalysis.prototype.updateMapWithEvents = function(sharedAI) { * so this can use the land regions already. */ -function Accessibility(rawState, terrainAnalyser){ +function Accessibility() { + +} + +copyPrototype(Accessibility, TerrainAnalysis); + +Accessibility.prototype.init = function(rawState, terrainAnalyser){ var self = this; this.Map(rawState, terrainAnalyser.map); - this.passMap = new Uint8Array(terrainAnalyser.length); + this.landPassMap = new Uint8Array(terrainAnalyser.length); + this.navalPassMap = new Uint8Array(terrainAnalyser.length); this.regionSize = []; - this.regionSize.push(0); + this.regionType = []; // "inaccessible", "land" or "water"; + // ID of the region associated with an array of region IDs. + this.regionLinks = []; // initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch); // However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.) // So start at 2. this.regionID = 2; - for (var i = 0; i < this.passMap.length; ++i) { - if (this.passMap[i] === 0 && this.map[i] !== 0) { // any non-painted, non-inacessible area. - this.regionSize.push(0); // updated - this.floodFill(i,this.regionID,false); - this.regionID++; - } else if (this.passMap[i] === 0) { // any non-painted, inacessible area. + + for (var i = 0; i < this.landPassMap.length; ++i) { + if (this.map[i] !== 0) { // any non-painted, non-inacessible area. + if (this.landPassMap[i] === 0 && this.floodFill(i,this.regionID,false)) + this.regionType[this.regionID++] = "land"; + if (this.navalPassMap[i] === 0 && this.floodFill(i,this.regionID,true)) + this.regionType[this.regionID++] = "water"; + } else if (this.landPassMap[i] === 0) { // any non-painted, inacessible area. this.floodFill(i,1,false); + this.floodFill(i,1,true); } } -} -copyPrototype(Accessibility, TerrainAnalysis); + + // calculating region links. Regions only touching diagonaly are not linked. + // since we're checking all of them, we'll check from the top left to the bottom right + var w = this.width; + for (var x = 0; x < this.width-1; ++x) + { + for (var y = 0; y < this.height-1; ++y) + { + // checking right. + var thisLID = this.landPassMap[x+y*w]; + var thisNID = this.navalPassMap[x+y*w]; + var rightLID = this.landPassMap[x+1+y*w]; + var rightNID = this.navalPassMap[x+1+y*w]; + var bottomLID = this.landPassMap[x+y*w+w]; + var bottomNID = this.navalPassMap[x+y*w+w]; + if (thisLID > 1) + { + if (rightNID > 1) + if (this.regionLinks[thisLID].indexOf(rightNID) === -1) + this.regionLinks[thisLID].push(rightNID); + if (bottomNID > 1) + if (this.regionLinks[thisLID].indexOf(bottomNID) === -1) + this.regionLinks[thisLID].push(bottomNID); + } + if (thisNID > 1) + { + if (rightLID > 1) + if (this.regionLinks[thisNID].indexOf(rightLID) === -1) + this.regionLinks[thisNID].push(rightLID); + if (bottomLID > 1) + if (this.regionLinks[thisNID].indexOf(bottomLID) === -1) + this.regionLinks[thisNID].push(bottomLID); + if (thisLID > 1) + if (this.regionLinks[thisNID].indexOf(thisLID) === -1) + this.regionLinks[thisNID].push(thisLID); + } + } + } + + //warn(uneval(this.regionLinks)); -Accessibility.prototype.getAccessValue = function(position){ + //Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255); + //Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255); +} + +Accessibility.prototype.getAccessValue = function(position, onWater) { var gamePos = this.gamePosToMapPos(position); - return this.passMap[gamePos[0] + this.width*gamePos[1]]; + if (onWater === true) + return this.navalPassMap[gamePos[0] + this.width*gamePos[1]]; + var ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]]; + if (ret === 1) + { + // quick spiral search. + var indx = [ [-1,-1],[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1],[0,-1]] + for (i in indx) + { + ret = this.landPassMap[gamePos[0]+indx[0] + this.width*(gamePos[1]+indx[0])] + if (ret !== undefined && ret !== 1) + return ret; + } + } + return ret; }; // Returns true if a point is deemed currently accessible (is not blocked by surrounding trees...) @@ -283,49 +356,233 @@ Accessibility.prototype.isAccessible = function(gameState, position, onLand){ // Return true if you can go from a point to a point without switching means of transport // Hardcore means is also checks for isAccessible at the end (it checks for either water or land though, beware). // This is a blind check and not a pathfinder: for all it knows there is a huge block of trees in the middle. -Accessibility.prototype.pathAvailable = function(gameState, start,end, hardcore){ +Accessibility.prototype.pathAvailable = function(gameState, start, end, onWater, hardcore){ var pstart = this.gamePosToMapPos(start); var istart = pstart[0] + pstart[1]*this.width; var pend = this.gamePosToMapPos(end); var iend = pend[0] + pend[1]*this.width; - - if (this.passMap[istart] === this.passMap[iend]) { - if (hardcore && (this.isAccessible(gameState, end,true) || this.isAccessible(gameState, end,false))) + if (onWater) + { + if (this.navalPassMap[istart] === this.navalPassMap[iend]) { + if (hardcore && this.isAccessible(gameState, end,false)) + return true; + else if (hardcore) + return false; return true; - else if (hardcore) - return false; - return true; + } + } else { + if (this.landPassMap[istart] === this.landPassMap[iend]) { + if (hardcore && this.isAccessible(gameState, end,true)) + return true; + else if (hardcore) + return false; + return true; + } } return false; }; -Accessibility.prototype.getRegionSize = function(position){ - var pos = this.gamePosToMapPos(position); - var index = pos[0] + pos[1]*this.width; - if (this.regionSize[this.passMap[index]] === undefined) - return 0; - return this.regionSize[this.passMap[index]]; -}; -Accessibility.prototype.getRegionSizei = function(index) { - if (this.regionSize[this.passMap[index]] === undefined) - return 0; - return this.regionSize[this.passMap[index]]; + +Accessibility.prototype.getTrajectTo = function(start, end, noBound) { + var pstart = this.gamePosToMapPos(start); + var istart = pstart[0] + pstart[1]*this.width; + var pend = this.gamePosToMapPos(end); + var iend = pend[0] + pend[1]*this.width; + + var onLand = true; + if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1) + onLand = false; + if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1) + return false; + + var endRegion = this.landPassMap[iend]; + if (endRegion <= 1 && this.navalPassMap[iend] > 1) + endRegion = this.navalPassMap[iend]; + else if (endRegion <= 1) + return false; + + if (onLand) + var startRegion = this.landPassMap[istart]; + else + var startRegion = this.navalPassMap[istart]; + + return this.getTrajectToIndex(startRegion, endRegion, noBound); +} + +// Return a "path" of accessibility indexes from one point to another, including the start and the end indexes (unless specified otherwise) +// this can tell you what sea zone you need to have a dock on, for example. +// assumes a land unit unless start point is over deep water. +// if the path is more complicated than "land->sea->land" (or "sea->land->sea"), it will run A* to try and figure it out +// Thus it can handle arbitrarily complicated paths (theoretically). +Accessibility.prototype.getTrajectToIndex = function(istart, iend, noBound){ + var startRegion = istart; + var currentRegion = istart; + + var endRegion = iend; + + // optimizations to avoid needless memory usage + // if it's the same, return the path + if (startRegion === endRegion) + return [startRegion]; + else if (this.regionLinks[startRegion].indexOf(endRegion) !== -1) + return [startRegion, endRegion]; + else + { + var rgs = this.regionLinks[startRegion]; + for (p in rgs) + { + if (this.regionLinks[rgs[p]].indexOf(endRegion) !== -1) + return [startRegion, rgs[p], endRegion]; + } + } + // it appears to be difficult. + // computing A* over a graph with all nodes equally good (might want to change this sometimes), currently it returns the shortest path switch-wise. + this.openList = []; + this.parentSquare = new Uint8Array(this.regionSize.length); + this.isOpened = new Boolean(this.regionSize.length); + this.gCostArray = new Uint8Array(this.regionSize.length); + + this.isOpened[currentRegion] = true; + this.openList.push(currentRegion); + this.gCostArray[currentRegion] = 0; + this.parentSquare[currentRegion] = currentRegion; + + var w = this.width; + var h = this.height; + + //creation of variables used in the loop + var found = false; + + // on to A* + while (found === false && this.openList.length !== 0) { + var currentDist = 300; + var ti = 0; + for (var i in this.openList) + { + var sum = this.gCostArray[this.openList[i]]; + if (sum < currentDist) + { + ti = i; + currentRegion = this.openList[i]; + currentDist = sum; + } + } + this.openList.splice(ti,1); + this.isOpened[currentRegion] = false; + + // special case, might make it faster (usually oceans connect multiple land masses, sometimes all of them) + if (this.regionType[currentRegion] == "water" && endLand) + { + var idx = this.regionLinks[currentRegion].indexOf(endRegion); + if (idx !== -1) + { + this.parentSquare[endRegion] = currentRegion; + this.gCostArray[endRegion] = this.gCostArray[currentRegion] + 1; + found = true; + break; + } + } + for (var i in this.regionLinks[currentRegion]) + { + var region = this.regionLinks[currentRegion][i]; + if(this.isOpened[region] === undefined) + { + this.parentSquare[region] = currentRegion; + this.gCostArray[region] = this.gCostArray[currentRegion] + 1; + this.openList.push(region); + this.isOpened[region] = true; + if (region === endRegion) + { + found = true; + break; + } + } else { + if (this.gCostArray[region] > 1 + this.gCostArray[currentRegion]) + { + this.parentSquare[region] = currentRegion; + this.gCostArray[region] = 1 + this.gCostArray[currentRegion]; + } + } + } + } + var path = []; + if (found) { + currentRegion = endRegion; + if (!noBound) + path.push(currentRegion); + while (this.parentSquare[currentRegion] !== startRegion) + { + currentRegion = this.parentSquare[currentRegion]; + path.push(currentRegion); + } + if (!noBound) + path.push(startRegion); + } else { + delete this.parentSquare; + delete this.isOpened; + delete this.gCostArray; + return false; + } + + delete this.parentSquare; + delete this.isOpened; + delete this.gCostArray; + + return path; }; -// Implementation of a fast flood fill. Reasonably good performances. Runs once at startup. +Accessibility.prototype.getRegionSize = function(position, onWater){ + var pos = this.gamePosToMapPos(position); + var index = pos[0] + pos[1]*this.width; + var ID = (onWater === true) ? this.navalPassMap[index] : this.landPassMap[index]; + if (this.regionSize[ID] === undefined) + return 0; + return this.regionSize[ID]; +}; + +Accessibility.prototype.getRegionSizei = function(index, onWater) { + if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined)) + return 0; + if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]]) + return this.regionSize[this.navalPassMap[index]]; + return this.regionSize[this.landPassMap[index]]; +}; + +// Implementation of a fast flood fill. Reasonably good performances for JS. // TODO: take big zones of impassable trees into account? Accessibility.prototype.floodFill = function(startIndex, value, onWater) { this.s = startIndex; - if (this.passMap[this.s] !== 0) { + if ((!onWater && this.landPassMap[this.s] !== 0) || (onWater && this.navalPassMap[this.s] !== 0) ) { return false; // already painted. } this.floodFor = "land"; - if (this.map[this.s] === 200 || (this.map[this.s] === 201 && onWater === true)) + if (this.map[this.s] === 0) + { + this.landPassMap[this.s] = 1; + this.navalPassMap[this.s] = 1; + return false; + } + if (onWater === true) + { + if (this.map[this.s] !== 200 && this.map[this.s] !== 201) + { + this.navalPassMap[this.s] = 1; // impassable for naval + return false; // do nothing + } this.floodFor = "water"; - else if (this.map[this.s] === 0) - this.floodFor = "impassable"; - + } else if (this.map[this.s] === 200) { + this.landPassMap[this.s] = 1; // impassable for land + return false; + } + + // here we'll be able to start. + for (var i = this.regionSize.length; i <= value; ++i) + { + this.regionLinks.push([]); + this.regionSize.push(0); + this.regionType.push("inaccessible"); + } var w = this.width; var h = this.height; @@ -347,11 +604,9 @@ Accessibility.prototype.floodFill = function(startIndex, value, onWater) var index = +newIndex + w*y; if (index < 0) break; - if (this.floodFor === "impassable" && this.map[index] === 0 && this.passMap[index] === 0) { + if (this.floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { loop = true; - } else if (this.floodFor === "land" && this.passMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { - loop = true; - } else if (this.floodFor === "water" && this.passMap[index] === 0 && (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) ) { + } else if (this.floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201)) { loop = true; } else { break; @@ -363,14 +618,12 @@ Accessibility.prototype.floodFill = function(startIndex, value, onWater) loop = true; do { var index = +newIndex + w*y; - if (this.floodFor === "impassable" && this.map[index] === 0 && this.passMap[index] === 0) { - this.passMap[index] = value; + + if (this.floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { + this.landPassMap[index] = value; this.regionSize[value]++; - } else if (this.floodFor === "land" && this.passMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { - this.passMap[index] = value; - this.regionSize[value]++; - } else if (this.floodFor === "water" && this.passMap[index] === 0 && (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) ) { - this.passMap[index] = value; + } else if (this.floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201)) { + this.navalPassMap[index] = value; this.regionSize[value]++; } else { break; @@ -378,17 +631,12 @@ Accessibility.prototype.floodFill = function(startIndex, value, onWater) if (index%w > 0) { - if (this.floodFor === "impassable" && this.map[index -1] === 0 && this.passMap[index -1] === 0) { + if (this.floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200) { if(!reachLeft) { IndexArray.push(index -1); reachLeft = true; } - } else if (this.floodFor === "land" && this.passMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200) { - if(!reachLeft) { - IndexArray.push(index -1); - reachLeft = true; - } - } else if (this.floodFor === "water" && this.passMap[index -1] === 0 && (this.map[index -1] === 200 || (this.map[index -1] === 201 && this.onWater)) ) { + } else if (this.floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === 200 || this.map[index -1] === 201)) { if(!reachLeft) { IndexArray.push(index -1); reachLeft = true; @@ -399,17 +647,12 @@ Accessibility.prototype.floodFill = function(startIndex, value, onWater) } if (index%w < w - 1) { - if (this.floodFor === "impassable" && this.map[index +1] === 0 && this.passMap[index +1] === 0) { + if (this.floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200) { if(!reachRight) { IndexArray.push(index +1); reachRight = true; } - } else if (this.floodFor === "land" && this.passMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200) { - if(!reachRight) { - IndexArray.push(index +1); - reachRight = true; - } - } else if (this.floodFor === "water" && this.passMap[index +1] === 0 && (this.map[index +1] === 200 || (this.map[index +1] === 201 && this.onWater)) ) { + } else if (this.floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === 200 || this.map[index +1] === 201)) { if(!reachRight) { IndexArray.push(index +1); reachRight = true; @@ -419,157 +662,81 @@ Accessibility.prototype.floodFill = function(startIndex, value, onWater) } } ++y; - } while (index/w < w) // should actually break + } while (index/w < w-1) // should actually break } return true; } -function landSizeCounter(rawState,terrainAnalyzer) { - var self = this; +// TODO: make it regularly update stone+metal mines and their resource levels. +// creates and maintains a map of unused resource density +// this also takes dropsites into account. +// resources that are "part" of a dropsite are not counted. +SharedScript.prototype.updateResourceMaps = function(sharedScript, events) { - this.passMap = terrainAnalyzer.map; - - var map = new Uint8Array(this.passMap.length); - this.Map(rawState,map); - - - for (var i = 0; i < this.passMap.length; ++i) { - if (this.passMap[i] !== 0) - this.map[i] = 255; - else - this.map[i] = 0; - } - - this.expandInfluences(); -} -copyPrototype(landSizeCounter, TerrainAnalysis); - -// Implementation of A* as a flood fill. Possibility of (clever) oversampling -// for efficiency or for disregarding too small passages. -// can operate over several turns, though default is only one turn. -landSizeCounter.prototype.getAccessibleLandSize = function(position, sampling, mode, OnlyBuildable, sizeLimit, iterationLimit) -{ - if (sampling === undefined) - this.Sampling = 1; - else - this.Sampling = sampling < 1 ? 1 : sampling; - - // this checks from the actual starting point. If that is inaccessible (0), it returns undefined; - if (position.length !== undefined) { - // this is an array - if (position[0] < 0 || this.gamePosToMapPos(position)[0] >= this.width || position[1] < 0 || this.gamePosToMapPos(position)[1] >= this.height) - return undefined; - - var s = this.gamePosToMapPos(position); - this.s = s[0] + w*s[1]; - if (this.map[this.s] === 0 || this.map[this.s] === 200 || (OnlyBuildable === true && this.map[this.s] === 201) ) { - return undefined; - } - } else { - this.s = position; - if (this.map[this.s] === 0 || this.map[this.s] === 200 || (OnlyBuildable === true && this.map[this.s] === 201) ) { - return undefined; + for (var resource in this.decreaseFactor){ + // if there is no resourceMap create one with an influence for everything with that resource + if (! this.resourceMaps[resource]){ + // We're creting them 8-bit. Things could go above 255 if there are really tons of resources + // But at that point the precision is not really important anyway. And it saves memory. + this.resourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.resourceMaps[resource].setMaxVal(255); + this.CCResourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.CCResourceMaps[resource].setMaxVal(255); } } - - if (mode === undefined) - this.mode = "default"; - else - this.mode = mode; - - if (sizeLimit === undefined) - this.sizeLimit = 300000; - else - this.sizeLimit = sizeLimit; - - var w = this.width; - var h = this.height; - // max map size is 512*512, this is higher. - this.iterationLimit = 300000; - if (iterationLimit !== undefined) - this.iterationLimit = iterationLimit; - - this.openList = []; - this.isOpened = new Boolean(this.map.length); - this.gCostArray = new Uint16Array(this.map.length); - - this.currentSquare = this.s; - this.isOpened[this.s] = true; - this.openList.push(this.s); - this.gCostArray[this.s] = 0; - - this.countedValue = 1; - this.countedArray = [this.s]; - - if (OnlyBuildable !== undefined) - this.onlyBuildable = OnlyBuildable; - else - this.onlyBuildable = true; - - return this.continueLandSizeCalculation(); -} -landSizeCounter.prototype.continueLandSizeCalculation = function() -{ - var w = this.width; - var h = this.height; - var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]; - var cost = [10,10,10,10,15,15,15,15]; - - //creation of variables used in the loop - var nouveau = false; - var shortcut = false; - var Sampling = this.Sampling; - var infinity = Math.min(); - var currentDist = infinity; - - var iteration = 0; - while (this.openList.length !== 0 && iteration < this.iterationLimit && this.countedValue < this.sizeLimit && this.countedArray.length < this.sizeLimit){ - currentDist = infinity; - for (var i in this.openList) - { - var sum = this.gCostArray[this.openList[i]]; - if (sum < currentDist) - { - this.currentSquare = this.openList[i]; - currentDist = sum; + // Look for destroy events and subtract the entities original influence from the resourceMap + // TODO: perhaps do something when dropsites appear/disappear. + for (var key in events) { + var e = events[key]; + if (e.type === "Destroy") { + if (e.msg.entityObj){ + var ent = e.msg.entityObj; + if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { + var resource = ent.resourceSupplyType().generic; + var x = Math.floor(ent.position()[0] / 4); + var z = Math.floor(ent.position()[1] / 4); + var strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); + + if (resource === "wood" || resource === "food") + { + this.resourceMaps[resource].addInfluence(x, z, 2, 5,'constant'); + this.resourceMaps[resource].addInfluence(x, z, 9.0, -strength,'constant'); + this.CCResourceMaps[resource].addInfluence(x, z, 15, -strength/2.0,'constant'); + } + else if (resource === "stone" || resource === "metal") + { + this.resourceMaps[resource].addInfluence(x, z, 8, 50); + this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/1.5); + this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/2.0,'constant'); + this.CCResourceMaps[resource].addInfluence(x, z, 30, -strength,'constant'); + } + } } - } - this.openList.splice(this.openList.indexOf(this.currentSquare),1); - - shortcut = false; - this.isOpened[this.currentSquare] = false; - for (var i in positions) { - var index = 0 + this.currentSquare + positions[i][0]*Sampling + w*Sampling*positions[i][1]; - if (this.passMap[index] !== 0 && this.passMap[index] !== 200 && this.map[index] >= Sampling && (!this.onlyBuildable || this.passMap[index] !== 201)) { - if(this.isOpened[index] === undefined) { - if (this.mode === "default") - this.countedValue++; - else if (this.mode === "array") - this.countedArray.push(index); - this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * Sampling; - this.openList.push(index); - this.isOpened[index] = true; + } else if (e.type === "Create") { + if (e.msg.entity){ + var ent = sharedScript._entities[e.msg.entity]; + if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure"){ + var resource = ent.resourceSupplyType().generic; + + var x = Math.floor(ent.position()[0] / 4); + var z = Math.floor(ent.position()[1] / 4); + var strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); + if (resource === "wood" || resource === "food") + { + this.CCResourceMaps[resource].addInfluence(x, z, 15, strength/2.0,'constant'); + this.resourceMaps[resource].addInfluence(x, z, 9.0, strength,'constant'); + this.resourceMaps[resource].addInfluence(x, z, 2, -5,'constant'); + } + else if (resource === "stone" || resource === "metal") + { + this.CCResourceMaps[resource].addInfluence(x, z, 30, strength,'constant'); + this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/1.5); + this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/2.0,'constant'); + this.resourceMaps[resource].addInfluence(x, z, 8, -50); + } } } } - iteration++; } - - if (iteration === this.iterationLimit && this.openList.length !== 0 && this.countedValue !== this.sizeLimit && this.countedArray.length !== this.sizeLimit) - { - // we've got to assume that we stopped because we reached the upper limit of iterations - return "toBeContinued"; - } - - delete this.parentSquare; - delete this.isOpened; - delete this.fCostArray; - delete this.gCostArray; - - if (this.mode === "default") - return this.countedValue; - else if (this.mode === "array") - return this.countedArray; - return undefined; -} +};