Index: ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js (revision 21825) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js (revision 21826) @@ -1,415 +1,415 @@ Trigger.prototype.tutorialGoals = [ { "instructions": markForTranslation("Welcome to the 0 A.D. tutorial."), }, { "instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { "instructions": markForTranslation("Select the citizen-soldier, right-click on a tree near the Civic Center to begin gathering Wood. Citizen Soldiers gather Wood faster than female citizens."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."), "hotkey": "session.batchtrain" } ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the HOPLITE icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Select the two idle female citizens and build a house nearby by selecting the house icon. Place the house by left-clicking on a piece of land."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a storehouse beside some nearby trees. They will begin to gather Wood when it's constructed."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Storehouse")) this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Train a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."), "hotkey": "session.batchtrain" } ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_javelinist_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Skirmisher icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Build a farmstead in an open space beside the Civic Center using any idle builders."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Farmstead")) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { "instructions": markForTranslation("Let's wait for the farmstead to be built."), "OnTrainingFinished": function(msg) { this.trainingDone = true; }, "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Farmstead")) this.NextGoal(); } }, { "instructions": markForTranslation("Once the farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a field beside the farmstead."), "Init": function() { this.farmStarted = false; }, "IsDone": function() { return this.farmStarted && this.trainingDone; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Field")) this.farmStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("The field's builders will now automatically begin gathering food from the field. Using the newly created group of skirmishers, get them to build another house nearby."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather Wood."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Hoplite icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a Wood icon.")); return; } this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Outpost")) this.NextGoal(); } }, { "instructions": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the 'II' icon (you have to wait for the outpost to be built first). This will allow Town Phase buildings to be constructed."), "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_generic"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { - "instructions": markForTranslation("While waiting for the phasing up, you may reassign your idle workers to gathering the resources you are short of."), + "instructions": markForTranslation("While waiting for the phasing up, you may reaffect your idle workers to gathering the resources you are short of."), "IsDone": function() { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnt = cmpPlayerManager.GetPlayerByID(this.playerID); let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched("phase_town_generic"); }, "OnResearchFinished": function(msg) { if (msg.tech == "phase_town_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Start training a batch of female citizens in the Civic Center and set its rally point to the farm (right click on it)."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_support_female_citizen" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the female citizen icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain") return; this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra house using any available builder units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Barracks")) this.NextGoal(); } }, { "instructions": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Defense Tower near your Outpost."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "DefenseTower")) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Blacksmith and research the Infantry Training technology (sword icon) to improve infantry hack attack."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Blacksmith")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy is coming. Train more soldiers to fight off the enemies."), "OnResearchFinished": function(msg) { this.LaunchAttack(); this.NextGoal(); } }, { "instructions": markForTranslation("Try to repel the attack."), "OnOwnershipChanged": function(msg) { if (msg.to != INVALID_PLAYER) return; if (this.IsAttackRepelled()) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources."), "Init": function() { this.marketStarted = false; this.templeStarted = false; }, "IsDone": function() { return this.marketStarted && this.templeStarted; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "repair") return; this.marketStarted = this.marketStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Market"); this.templeStarted = this.templeStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Temple"); if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phase change, you may train more soldiers at the barracks."), "OnResearchFinished": function(msg) { if (msg.tech == "phase_city_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Now that you are in City Phase, build a fortress nearby (gather some stone first if needed) and then use it to construct 2 Battering Rams."), "Init": function() { this.ramCount = 0; }, "IsDone": function() { return this.ramCount > 1; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate == "units/spart_mechanical_siege_ram") ++this.ramCount; if (this.IsDone()) { this.RemoveChampions(); this.NextGoal(); } } }, { "instructions": [ markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once The enemy's base has been spotted, send your siege weapons and all remaining soldiers to destroy it.\n"), markForTranslation("Female citizens should continue to gather resources.") ], "OnOwnershipChanged": function(msg) { if (msg.from != this.enemyID) return; if (TriggerHelper.EntityMatchesClassList(msg.entity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy has been defeated. These tutorial tasks are now completed."), } ]; Trigger.prototype.LaunchAttack = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); let target = entities.find(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_Identity).HasClass("DefenseTower")) || entities.find(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_Identity).HasClass("CivilCentre")); let position = Engine.QueryInterface(target, IID_Position).GetPosition2D(); this.attackers = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_UnitAI) && Engine.QueryInterface(e, IID_Identity).HasClass("CitizenSoldier") ); this.attackers.forEach(e => { Engine.QueryInterface(e, IID_UnitAI).WalkAndFight(position.x, position.y, { "attack": ["Unit"] }, false); }); }; Trigger.prototype.IsAttackRepelled = function() { return !this.attackers.some(e => Engine.QueryInterface(e, IID_Health) && Engine.QueryInterface(e, IID_Health).GetHitpoints() > 0); }; Trigger.prototype.RemoveChampions = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let champions = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity).HasClass("Champion")); let keep = 6; for (let ent of champions) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) Engine.DestroyEntity(ent); else if (--keep < 0) cmpHealth.Kill(); } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.enemyID = 2; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 21825) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 21826) @@ -1,2171 +1,2171 @@ var PETRA = function(m) { /** * This is an attack plan: * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units are built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. */ m.AttackPlan = function(gameState, Config, uniqueID, type, data) { this.Config = Config; this.name = uniqueID; this.type = type || "Attack"; this.state = "unexecuted"; this.forced = false; // true when this attacked has been forced to help an ally if (data && data.target) { this.target = data.target; this.targetPos = this.target.position(); this.targetPlayer = this.target.owner(); } else { this.target = undefined; this.targetPos = undefined; this.targetPlayer = undefined; } this.uniqueTargetId = data && data.uniqueTargetId || undefined; // get a starting rallyPoint ... will be improved later let rallyPoint; let rallyAccess; let allAccesses = {}; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let access = m.getLandAccess(gameState, base.anchor); if (!rallyPoint) { rallyPoint = base.anchor.position(); rallyAccess = access; } if (!allAccesses[access]) allAccesses[access] = base.anchor.position(); } if (!rallyPoint) // no base ? take the position of any of our entities { for (let ent of gameState.getOwnEntities().values()) { if (!ent.position()) continue; let access = m.getLandAccess(gameState, ent); rallyPoint = ent.position(); rallyAccess = access; allAccesses[access] = rallyPoint; break; } if (!rallyPoint) { this.failed = true; return false; } } this.rallyPoint = rallyPoint; this.overseas = 0; if (gameState.ai.HQ.navalMap) { for (let structure of gameState.getEnemyStructures().values()) { if (this.target && structure.id() != this.target.id()) continue; if (!structure.position()) continue; let access = m.getLandAccess(gameState, structure); if (access in allAccesses) { this.overseas = 0; this.rallyPoint = allAccesses[access]; break; } else if (!this.overseas) { let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); if (!sea) { if (this.target) { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " + this.target.templateName() + " indices " + rallyAccess + " " + access); this.failed = true; return false; } continue; } this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); } } } this.paused = false; this.maxCompletingTime = 0; // priority of the queues we'll create. let priority = 70; // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; // neededShips is the minimal number of ships which should be available for transport if (type == "Rush") { priority = 250; this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [["strength", 1]] }; if (data && data.targetSize) this.unitStat.Infantry.targetSize = data.targetSize; this.neededShips = 1; } else if (type == "Raid") { priority = 150; this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength", 1] ] }; this.neededShips = 1; } else if (type == "HugeAttack") { priority = 90; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"], "interests": [["strength", 3]] }; this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"], "interests": [["strength", 2]] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [["strength", 2]] }; this.neededShips = 5; } else { priority = 70; this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Ranged"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Melee"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [["strength", 1]] }; this.neededShips = 3; } // Put some randomness on the attack size let variation = randFloat(0.8, 1.2); // and lower priority and smaller sizes for easier difficulty levels if (this.Config.difficulty < 2) { priority *= 0.6; variation *= 0.5; } else if (this.Config.difficulty < 3) { priority *= 0.8; variation *= 0.8; } for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); } // change the sizes according to max population this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrders = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders // some variables used during the attack this.position5TurnsAgo = [0, 0]; this.lastPosition = [0, 0]; this.position = [0, 0]; this.isBlocked = false; // true when this attack faces walls return true; }; m.AttackPlan.prototype.init = function(gameState) { this.queue = gameState.ai.queues["plan_" + this.name]; this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name)); this.unitCollection.registerUpdates(); this.unit = {}; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (let cat in this.unitStat) { let Unit = this.unitStat[cat]; this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[cat].registerUpdates(); if (this.canBuildUnits) this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); } }; m.AttackPlan.prototype.getName = function() { return this.name; }; m.AttackPlan.prototype.getType = function() { return this.type; }; m.AttackPlan.prototype.isStarted = function() { return this.state !== "unexecuted" && this.state !== "completing"; }; m.AttackPlan.prototype.isPaused = function() { return this.paused; }; m.AttackPlan.prototype.setPaused = function(boolValue) { this.paused = boolValue; }; /** * Returns true if the attack can be executed at the current time * Basically it checks we have enough units. */ m.AttackPlan.prototype.canStart = function() { if (!this.canBuildUnits) return true; for (let unitCat in this.unitStat) if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) return false; return true; }; m.AttackPlan.prototype.mustStart = function() { if (this.isPaused()) return false; if (!this.canBuildUnits) return this.unitCollection.hasEntities(); let MaxReachedEverywhere = true; let MinReachedEverywhere = true; for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit.targetSize) MaxReachedEverywhere = false; if (this.unit[unitCat].length < Unit.minSize) { MinReachedEverywhere = false; break; } } if (MaxReachedEverywhere) return true; if (MinReachedEverywhere) return this.type == "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 50; return false; }; m.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } this.forced = true; }; /** Adds a build order. If resetQueue is true, this will reset the queue. */ m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; let Unit = this.unitStat[name]; this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[name].registerUpdates(); this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); if (resetQueue) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } } }; m.AttackPlan.prototype.addSiegeUnits = function(gameState) { if (this.siegeState == 2 || this.state !== "unexecuted") return false; let civ = gameState.getPlayerCiv(); let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; let hasTrainer = [false, false, false]; for (let ent of gameState.getOwnTrainingFacilities().values()) { let trainables = ent.trainableEntities(civ); if (!trainables) continue; for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.available(gameState)) continue; for (let i = 0; i < classes.length; ++i) if (classes[i].every(c => template.hasClass(c))) hasTrainer[i] = true; } } if (hasTrainer.every(e => !e)) return false; let i = this.name % classes.length; for (let k = 0; k < classes.length; ++k) { if (hasTrainer[i]) break; i = ++i % classes.length; } this.siegeState = 2; let targetSize; if (this.Config.difficulty < 3) targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); else targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); if (!targetSize) return true; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; this.addBuildOrder(gameState, "Siege", stat, true); return true; }; /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ m.AttackPlan.prototype.updatePreparation = function(gameState) { // the completing step is used to return resources and regroup the units // so we check that we have no more forced order before starting the attack if (this.state == "completing") { // if our target was destroyed, go back to "unexecuted" state if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { this.state = "unexecuted"; this.target = undefined; } else { // check that all units have finished with their transport if needed if (this.waitingForTransport()) return 1; // bloqued units which cannot finish their order should not stop the attack if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) return 1; return 2; } } if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) this.debugAttack(); // if we need a transport, wait for some transport ships if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) return 1; if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) this.assignUnits(gameState); if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations // Fasten the end game. if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits()) { let totEnemies = 0; let hasEnemies = false; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i]) continue; hasEnemies = true; totEnemies += gameState.getEnemyUnits(i).length; } if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies) this.forceStart(); } // special case: if we've reached max pop, and we can start the plan, start it. if (gameState.getPopulationMax() - gameState.getPopulation() < 5) { let lengthMin = 16; if (gameState.getPopulationMax() < 300) lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); if (this.canStart() || this.unitCollection.length > lengthMin) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } else // Abort the plan so that its units will be reassigned to other plans. { if (this.Config.debug > 1) { let am = gameState.ai.HQ.attackManager; API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + " rush " + am.upcomingAttacks.Rush.length + " attack " + am.upcomingAttacks.Attack.length + " huge " + am.upcomingAttacks.HugeAttack.length); API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + " rush " + am.startedAttacks.Rush.length + " attack " + am.startedAttacks.Attack.length + " huge " + am.startedAttacks.HugeAttack.length); } return 0; } } else if (this.mustStart()) { if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) { // keep on while the units finish being trained, then we'll start this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); return 1; } } else { if (this.canBuildUnits) { // We still have time left to recruit units and do stuffs. if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0) this.addSiegeUnits(gameState); this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrders.length) return 0; // will abort the plan } return 1; } // if we're here, it means we must start this.state = "completing"; // Raids have their predefined target if (!this.target && !this.chooseTarget(gameState)) return 0; if (!this.overseas) this.getPathToTarget(gameState); if (this.type == "Raid") this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; else { if (this.type == "Rush" || this.forced) this.maxCompletingTime = gameState.ai.elapsedTime + 40; else this.maxCompletingTime = gameState.ai.elapsedTime + 60; // warn our allies so that they can help if possible if (!this.requested) Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer }); } // Remove those units which were in a temporary bombing attack for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) continue; unitIds.delete(entId); ent.stopMoving(); } } let rallyPoint = this.rallyPoint; let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); for (let ent of this.unitCollection.values()) { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) { ent.setMetadata(PlayerID, "plan", -1); continue; } ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "subrole", "completing"); let queued = false; if (ent.resourceCarrying() && ent.resourceCarrying().length) queued = m.returnResources(gameState, ent); let index = m.getLandAccess(gameState, ent); if (index == rallyIndex) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); } // reset all queued units let plan = this.name; gameState.ai.queueManager.removeQueue("plan_" + plan); gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); return 1; }; m.AttackPlan.prototype.trainMoreUnits = function(gameState) { // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. for (let order of this.buildOrders) { let special = "Plan_" + this.name + "_" + order[4]; let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); order[0] = order[2].length + aQueued; } this.buildOrders.sort((a, b) => { let va = a[0]/a[3].targetSize - a[3].priority; if (a[0] >= a[3].targetSize) va += 1000; let vb = b[0]/b[3].targetSize - b[3].priority; if (b[0] >= b[3].targetSize) vb += 1000; return va - vb; }); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) { API3.warn("===================================="); API3.warn("======== build order for plan " + this.name); for (let order of this.buildOrders) { let specialData = "Plan_"+this.name+"_"+order[4]; let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); } API3.warn("===================================="); } let firstOrder = this.buildOrders[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Hero") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueChamp; if (queue.length() <= 5) { let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, // effectively removing the unit from the plan. if (template === undefined) { if (this.Config.debug > 1) API3.warn("attack no template found " + firstOrder[1]); delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. this.buildOrders.splice(0, 1); } else { if (this.Config.debug > 2) API3.warn("attack template " + template + " added for plan " + this.name); let max = firstOrder[3].batchSize; let specialData = "Plan_" + this.name + "_" + firstOrder[4]; let data = { "plan": this.name, "special": specialData, "base": 0 }; data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); if (trainingPlan.template) queue.addPlan(trainingPlan); else if (this.Config.debug > 1) API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + " build3 " + uneval(firstOrder[3].interests)); } } } }; m.AttackPlan.prototype.assignUnits = function(gameState) { let plan = this.name; let added = false; // If we can not build units, assign all available except those affected to allied defense to the current attack if (!this.canBuildUnits) { for (let ent of gameState.getOwnUnits().values()) { if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } if (this.type == "Raid") { // Raid are fast cavalry attack: assign all cav except some for hunting let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent)) continue; if (num++ < 2) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } // Assign all units without specific role for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) { if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Add units previously in a plan, but which left it because needed for defense or attack finished for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Finally add also some workers, // If Rush, assign all kind of workers, keeping only a minimum number of defenders // Otherwise, assign only some idle workers if too much of them let num = 0; let numbase = {}; let keep = this.type != "Rush" ? 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; keep = Math.round(this.Config.popScaling * keep); for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) { if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) continue; let baseID = ent.getMetadata(PlayerID, "base"); if (baseID) numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; else { API3.warn("Petra problem ent without base "); m.dumpEntity(ent); continue; } if (num++ < keep || numbase[baseID] < 5) continue; if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; }; m.AttackPlan.prototype.isAvailableUnit = function(gameState, ent) { if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return false; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) return false; return true; }; /** Reassign one (at each turn) Cav unit to fasten raid preparation. */ m.AttackPlan.prototype.reassignCavUnit = function(gameState) { for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) continue; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); ent.setMetadata(PlayerID, "plan", raid.name); this.unitCollection.updateEnt(ent); raid.unitCollection.updateEnt(ent); return; } }; m.AttackPlan.prototype.chooseTarget = function(gameState) { if (this.targetPlayer === undefined) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; } this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) { if (this.uniqueTargetId) return false; // may-be all our previous enemey target (if not recomputed here) have been destroyed ? this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) return false; } this.targetPos = this.target.position(); // redefine a new rally point for this target if we have a base on the same land // find a new one on the pseudo-nearest base (dist weighted by the size of the island) let targetIndex = m.getLandAccess(gameState, this.target); let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); if (targetIndex != rallyIndex) { let distminSame = Math.min(); let rallySame; let distminDiff = Math.min(); let rallyDiff; for (let base of gameState.ai.HQ.baseManagers) { let anchor = base.anchor; if (!anchor || !anchor.position()) continue; let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos); if (base.accessIndex == targetIndex) { if (dist >= distminSame) continue; distminSame = dist; rallySame = anchor.position(); } else { dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); if (dist >= distminDiff) continue; distminDiff = dist; rallyDiff = anchor.position(); } } if (rallySame) { this.rallyPoint = rallySame; this.overseas = 0; } else if (rallyDiff) { rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); this.rallyPoint = rallyDiff; let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); if (sea) { this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); } else { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" + " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName()); return false; } } } else if (this.overseas) this.overseas = 0; return true; }; /** * sameLand true means that we look for a target for which we do not need to take a transport */ m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) { this.isBlocked = false; // Temporary variables needed by isValidTarget this.gameState = gameState; this.sameLand = sameLand && sameLand > 1 ? sameLand : false; let targets; if (this.uniqueTargetId) { targets = new API3.EntityCollection(gameState.sharedScript); let ent = gameState.getEntityById(this.uniqueTargetId); if (ent) targets.addEnt(ent); } else { if (this.type == "Raid") targets = this.raidTargetFinder(gameState); else if (this.type == "Rush" || this.type == "Attack") { targets = this.rushTargetFinder(gameState, this.targetPlayer); if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) targets = this.defaultTargetFinder(gameState, this.targetPlayer); } else targets = this.defaultTargetFinder(gameState, this.targetPlayer); } if (!targets.hasEntities()) return undefined; // picking the nearest target let target; let minDist = Math.min(); for (let ent of targets.values()) { if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id()))) continue; // Do not bother with some pointless targets if (!this.isValidTarget(ent)) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // In normal attacks, disfavor fields if (this.type != "Rush" && this.type != "Raid" && ent.hasClass("Field")) dist += 100000; if (dist < minDist) { minDist = dist; target = ent; } } if (!target) return undefined; // Check that we can reach this target target = this.checkTargetObstruction(gameState, target, position); if (!target) return undefined; if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic")) gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]); // Rushes can change their enemy target if nothing found with the preferred enemy // Obstruction also can change the enemy target this.targetPlayer = target.owner(); return target; }; /** * Default target finder aims for conquest critical targets * We must apply the *same* selection (isValidTarget) as done in getNearestTarget */ m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); if (gameState.getVictoryConditions().has("wonder")) for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("regicide")) for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("capture_the_relic")) for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) targets.addEnt(ent); targets = targets.filter(this.isValidTarget, this); if (targets.hasEntities()) return targets; let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); targets = validTargets.filter(API3.Filters.byClass("CivCentre")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); // If there's nothing, attack anything else that's less critical if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Town")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Village")); // No buildings, attack anything conquest critical, units included. // TODO Should add naval attacks against the last remaining ships. if (!targets.hasEntities()) targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")). filter(API3.Filters.not(API3.Filters.byClass("Ship"))); return targets; }; m.AttackPlan.prototype.isValidTarget = function(ent) { if (!ent.position()) return false; if (this.sameLand && m.getLandAccess(this.gameState, ent) != this.sameLand) return false; return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length; }; /** Rush target finder aims at isolated non-defended buildings */ m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); let buildings; if (playerEnemy !== undefined) buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); else buildings = gameState.getEnemyStructures().toEntityArray(); if (!buildings.length) return targets; this.position = this.unitCollection.getCentrePosition(); if (!this.position) this.position = this.rallyPoint; let target; let minDist = Math.min(); for (let building of buildings) { if (building.owner() == 0) continue; if (building.hasDefensiveFire()) continue; if (!this.isValidTarget(building)) continue; let pos = building.position(); let defended = false; for (let defense of buildings) { if (!defense.hasDefensiveFire()) continue; let dist = API3.SquareVectorDistance(pos, defense.position()); if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 { defended = true; break; } } if (defended) continue; let dist = API3.SquareVectorDistance(pos, this.position); if (dist > minDist) continue; minDist = dist; target = building; } if (target) targets.addEnt(target); if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) targets = this.rushTargetFinder(gameState); return targets; }; /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ m.AttackPlan.prototype.raidTargetFinder = function(gameState) { let targets = new API3.EntityCollection(gameState.sharedScript); for (let targetId of gameState.ai.HQ.defenseManager.targetList) { let target = gameState.getEntityById(targetId); if (target && target.position()) targets.addEnt(target); } return targets; }; /** * Check that we can have a path to this target * otherwise we may be blocked by walls and try to react accordingly * This is done only when attacker and target are on the same land */ m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) { if (m.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) return target; let targetPos = target.position(); let startPos = { "x": position[0], "y": position[1] }; let endPos = { "x": targetPos[0], "y": targetPos[1] }; let blocker; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); if (!path.length) return undefined; let pathPos = [path[0].x, path[0].y]; let dist = API3.VectorDistance(pathPos, targetPos); let radius = target.obstructionRadius().max; for (let struct of gameState.getEnemyStructures().values()) { if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) continue; // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate if (dist < radius + 10 && !struct.hasClass("Gates")) continue; // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m // in the target direction would bring us inside its obstruction. let structPos = struct.position(); let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; if (struct.get("Obstruction/Static")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Static/@width"); let depth = +struct.get("Obstruction/Static/@depth"); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } } else if (struct.get("Obstruction/Obstructions")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Obstructions/Door/@width"); let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); let doorHalfWidth = width / 2; width += +struct.get("Obstruction/Obstructions/Left/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); width += +struct.get("Obstruction/Obstructions/Right/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } // check that the path does not cross this gate (could happen if not locked) for (let i = 1; i < path.length; ++i) { let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; if (v1 * v2 < 0) { let u0 = (u1*v2 - u2*v1) / (v2-v1); if (Math.abs(u0) > doorHalfWidth) continue; blocker = struct; break; } } if (blocker) break; } else if (struct.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); if (x*x + y*y < r*r) { blocker = struct; break; } } } if (blocker && blocker.hasClass("StoneWall")) { /* if (this.hasSiegeUnits()) { */ this.isBlocked = true; return blocker; /* } return undefined; */ } else if (blocker) { this.isBlocked = true; return blocker; } return target; }; m.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) { let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endAccess = m.getLandAccess(gameState, this.target); if (startAccess != endAccess) return false; Engine.ProfileStart("AI Compute path"); let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); this.path = []; this.path.push(this.targetPos); for (let p in path) this.path.push([path[p].x, path[p].y]); this.path.push(this.rallyPoint); this.path.reverse(); // Change the rally point to something useful if (!fixedRallyPoint) this.setRallyPoint(gameState); Engine.ProfileStop(); return true; }; /** Set rally point at the border of our territory */ m.AttackPlan.prototype.setRallyPoint = function(gameState) { for (let i = 0; i < this.path.length; ++i) { if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) continue; if (i === 0) this.rallyPoint = this.path[0]; else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) { this.rallyPoint = this.path[i-2]; this.path.splice(0, i-2); } else { this.rallyPoint = this.path[i-1]; this.path.splice(0, i-1); } break; } }; /** * Executes the attack plan, after this is executed the update function will be run every turn * If we're here, it's because we have enough units. */ m.AttackPlan.prototype.StartAttack = function(gameState) { if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); // if our target was destroyed during preparation, choose a new one if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) && !this.chooseTarget(gameState)) return false; // erase our queue. This will stop any leftover unit from being trained. gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); for (let ent of this.unitCollection.values()) { ent.setMetadata(PlayerID, "subrole", "walking"); let stance = ent.isPackable() ? "standground" : "aggressive"; if (ent.getStance() != stance) ent.setStance(stance); } let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let targetAccess = m.getLandAccess(gameState, this.target); if (rallyAccess == targetAccess) { if (!this.path) this.getPathToTarget(gameState, true); if (!this.path || !this.path[0][0] || !this.path[0][1]) return false; this.overseas = 0; this.state = "walking"; this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); } else { this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess); if (!this.overseas) return false; this.state = "transporting"; // TODO require a global transport for the collection, // and put back its state to "walking" when the transport is finished for (let ent of this.unitCollection.values()) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos); } return true; }; /** Runs every turn after the attack is executed */ m.AttackPlan.prototype.update = function(gameState, events) { if (!this.unitCollection.hasEntities()) return 0; Engine.ProfileStart("Update Attack"); this.position = this.unitCollection.getCentrePosition(); // we are transporting our units, let's wait // TODO instead of state "arrived", made a state "walking" with a new path if (this.state == "transporting") this.UpdateTransporting(gameState, events); if (this.state == "walking" && !this.UpdateWalking(gameState, events)) { Engine.ProfileStop(); return 0; } if (this.state == "arrived") { // let's proceed on with whatever happens now. this.state = ""; this.startingAttack = true; this.unitCollection.forEach(ent => { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); if (this.type == "Rush") // try to find a better target for rush { let newtarget = this.getNearestTarget(gameState, this.position); if (newtarget) { this.target = newtarget; this.targetPos = this.target.position(); } } } // basic state of attacking. if (this.state == "") { // First update the target and/or its position if needed if (!this.UpdateTarget(gameState)) { Engine.ProfileStop(); return false; } let time = gameState.ai.elapsedTime; let attackedByStructure = {}; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); let ourUnit = gameState.getEntityById(evt.target); if (!ourUnit || !attacker || !attacker.position()) continue; if (!attacker.hasClass("Unit")) { attackedByStructure[evt.target] = true; continue; } if (m.isSiegeUnit(ourUnit)) { // if our siege units are attacked, we'll send some units to deal with enemies. let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // And if this attacker is a non-ranged siege unit and our unit also, attack it if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) { ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { // do not react if our melee units are attacked by ranged one and we are blocked by walls // TODO check that the attacker is from behind the wall continue; } else if (m.isSiegeUnit(attacker)) { // if our unit is attacked by a siege unit, we'll send some melee units to help it. let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { // Look first for nearby units to help us if possible let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); for (let ent of collec.values()) { if (m.isSiegeUnit(ent)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) continue; } ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate let orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } } let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); // Count the number of times an enemy is targeted, to prevent all units to follow the same target let unitTargets = {}; for (let ent of this.unitCollection.values()) { if (ent.hasClass("Ship")) // TODO What to do with ships continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || target.hasClass("Structure")) continue; if (!(targetId in unitTargets)) { if (m.isSiegeUnit(target) || target.hasClass("Hero")) unitTargets[targetId] = -8; else if (target.hasClass("Champion") || target.hasClass("Ship")) unitTargets[targetId] = -5; else unitTargets[targetId] = -3; } ++unitTargets[targetId]; } let veto = {}; for (let target in unitTargets) if (unitTargets[target] > 0) veto[target] = true; let targetClassesUnit; let targetClassesSiege; if (this.type == "Rush") targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto }; else { if (this.target.hasClass("Fortress")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto }; else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; else targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Fortress"], "vetoEntities": veto }; } if (this.target.hasClass("Structure")) targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto }; else targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto }; // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later if (this.target.hasDefensiveFire()) { targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); } if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) this.unitCollUpdateArray = this.unitCollection.toIdArray(); // Let's check a few units each time we update (currently 10) except when attack starts let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; for (let check = 0; check < lgth; check++) { let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); if (!ent || !ent.position()) continue; - // Do not reassign units which have reacted to an attack in that same turn + // Do not reaffect units which have reacted to an attack in that same turn if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) continue; let targetId; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) targetId = orderData[0].target; // update the order if needed let needsUpdate = false; let maybeUpdate = false; let siegeUnit = m.isSiegeUnit(ent); if (ent.isIdle()) needsUpdate = true; else if (siegeUnit && targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (!target.hasClass("Structure")) maybeUpdate = true; } else if (targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (target.hasClass("Ship") && !ent.hasClass("Ship")) maybeUpdate = true; else if (attackedByStructure[ent.id()] && target.hasClass("Field")) maybeUpdate = true; else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") maybeUpdate = true; } // don't update too soon if not necessary if (!needsUpdate) { if (!maybeUpdate) continue; let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) continue; } ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); let range = 60; let attackTypes = ent.attackTypes(); if (this.isBlocked) { if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = ent.attackRange("Ranged").max; else if (attackTypes && attackTypes.indexOf("Melee") !== -1) range = ent.attackRange("Melee").max; else range = 10; } else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = 30 + ent.attackRange("Ranged").max; else if (ent.hasClass("Cavalry")) range += 30; range = range * range; let entAccess = m.getLandAccess(gameState, ent); // Checking for gates if we're a siege unit. if (siegeUnit) { let mStruct = enemyStructures.filter(enemy => { if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (enemy.foundationProgress() == 0) return false; if (m.getLandAccess(gameState, enemy) != entAccess) return false; return true; }).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else { if (!ent.hasClass("Ranged")) { let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); } } else { let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); let mUnit = enemyUnits.filter(enemy => { if (!enemy.position()) return false; if (enemy.hasClass("Animal")) return false; if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") return false; let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); if (dist > range) return false; if (m.getLandAccess(gameState, enemy) != entAccess) return false; // if already too much units targeting this enemy, let's continue towards our main target if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) return false; enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); return true; }, this).toEntityArray(); if (mUnit.length) { mUnit.sort((unitA, unitB) => { let vala = unitA.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitA.classes())) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitB.classes())) valb += 100; let distA = unitA.getMetadata(PlayerID, "distance"); let distB = unitB.getMetadata(PlayerID, "distance"); if (distA && distB) { vala -= distA; valb -= distB; } if (veto[unitA.id()]) vala -= 20000; if (veto[unitB.id()]) valb -= 20000; return valb - vala; }); let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); } else if (this.isBlocked) ent.attack(this.target.id(), false); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) { let targetClasses = targetClassesUnit; if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything { if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; else targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; } else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else { let mStruct = enemyStructures.filter(enemy => { if (this.isBlocked && enemy.id() != this.target.id()) return false; if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (m.getLandAccess(gameState, enemy) != entAccess) return false; return true; }, this).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), false); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit { let distmin = Math.min(); let attacker; this.unitCollection.forEach(unit => { if (!unit.position()) return; if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); }); if (attacker) ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } } } } this.unitCollUpdateArray.splice(0, lgth); this.startingAttack = false; // check if this enemy has resigned if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) this.target = undefined; } this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) { let done = true; for (let ent of this.unitCollection.values()) { if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] }); else if (this.Config.debug > 1) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] }); if (!done) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; } if (done) { this.state = "arrived"; return; } // if we are attacked while waiting the rest of the army, retaliate for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !gameState.getEntityById(evt.target)) continue; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.isIdle()) continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } break; } }; m.AttackPlan.prototype.UpdateWalking = function(gameState, events) { // we're marching towards the target // Let's check if any of our unit has been attacked. // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building // or if we reached the enemy base. Different plans may react differently. let attackedNB = 0; let attackedUnitNB = 0; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) { attackedNB++; if (attacker.hasClass("Unit")) attackedUnitNB++; } } // Are we arrived at destination ? if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) { if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) { this.state = "arrived"; return true; } } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units let farthest = 0; let farthestEnt; for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) { let dist = API3.SquareVectorDistance(ent.position(), this.position); if (dist < farthest) continue; farthest = dist; farthestEnt = ent; } if (farthestEnt) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) { if (!this.path[0][0] || !this.path[0][1]) API3.warn("Start: Problem with path " + uneval(this.path)); // We're stuck, presumably. Check if there are no walls just close to us. for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) { if (API3.SquareVectorDistance(this.position, ent.position()) > 800) continue; let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; // there are walls, so check if we can attack if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); this.state = "arrived"; return true; } // abort plan if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); return false; } // this.unitCollection.move(this.path[0][0], this.path[0][1]); this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); } // check if our units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) { this.path.shift(); if (this.path.length) this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); else { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } } return true; }; m.AttackPlan.prototype.UpdateTarget = function(gameState) { // First update the target position in case it's a unit (and check if it has garrisoned) if (this.target && this.target.hasClass("Unit")) { this.targetPos = this.target.position(); if (!this.targetPos) { let holder = m.getHolder(gameState, this.target); if (holder && gameState.isPlayerEnemy(holder.owner())) { this.target = holder; this.targetPos = holder.position(); } else this.target = undefined; } } // Then update the target if needed: if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; if (this.target && this.target.owner() !== this.targetPlayer) this.target = undefined; } if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned this.target = undefined; if (!this.target || !gameState.getEntityById(this.target.id())) { if (this.Config.debug > 1) API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); let accessIndex = this.getAttackAccess(gameState); this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.uniqueTargetId) return false; // Check if we could help any current attack let attackManager = gameState.ai.HQ.attackManager; for (let attackType in attackManager.startedAttacks) { for (let attack of attackManager.startedAttacks[attackType]) { if (attack.name == this.name) continue; if (!attack.target || !gameState.getEntityById(attack.target.id()) || !gameState.isPlayerEnemy(attack.target.owner())) continue; if (accessIndex != m.getLandAccess(gameState, attack.target)) continue; if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned continue; if (!gameState.isPlayerEnemy(attack.targetPlayer)) continue; this.target = attack.target; this.targetPlayer = attack.targetPlayer; this.targetPos = this.target.position(); return true; } } // If not, let's look for another enemy if (!this.target) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.Config.debug > 1) API3.warn("No new target found. Remaining units " + this.unitCollection.length); return false; } } if (this.Config.debug > 1) API3.warn("We will help one of our other attacks"); } this.targetPos = this.target.position(); } return true; }; /** reset any units */ m.AttackPlan.prototype.Abort = function(gameState) { this.unitCollection.unregister(); if (this.unitCollection.hasEntities()) { // If the attack was started, look for a good rallyPoint to withdraw let rallyPoint; if (this.isStarted()) { let access = this.getAttackAccess(gameState); let dist = Math.min(); if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) { rallyPoint = this.rallyPoint; dist = API3.SquareVectorDistance(this.position, rallyPoint); } // Then check if we have a nearer base (in case this attack has captured one) for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (m.getLandAccess(gameState, base.anchor) != access) continue; let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); if (newdist > dist) continue; dist = newdist; rallyPoint = base.anchor.position(); } } for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") == "attack") ent.stopMoving(); if (rallyPoint) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); this.removeUnit(ent); } } for (let unitCat in this.unitStat) this.unit[unitCat].unregister(); gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); }; m.AttackPlan.prototype.removeUnit = function(ent, update) { if (ent.getMetadata(PlayerID, "role") == "attack") { if (ent.hasClass("CitizenSoldier")) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); } ent.setMetadata(PlayerID, "plan", -1); if (update) this.unitCollection.updateEnt(ent); }; m.AttackPlan.prototype.checkEvents = function(gameState, events) { for (let evt of events.EntityRenamed) { if (!this.target || this.target.id() != evt.entity) continue; if (this.type == "Raid" && !this.isStarted()) this.target = undefined; else this.target = gameState.getEntityById(evt.newentity); if (this.target) this.targetPos = this.target.position(); } for (let evt of events.OwnershipChanged) // capture event if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) this.target = undefined; for (let evt of events.PlayerDefeated) { if (this.targetPlayer !== evt.playerId) continue; this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); this.target = undefined; } if (!this.overseas || this.state !== "unexecuted") return; // let's check if an enemy has built a structure at our access for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.position() || !ent.hasClass("Structure")) continue; if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = m.getLandAccess(gameState, ent); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; this.overseas = 0; this.rallyPoint = base.anchor.position(); } } }; m.AttackPlan.prototype.waitingForTransport = function() { for (let ent of this.unitCollection.values()) if (ent.getMetadata(PlayerID, "transport") !== undefined) return true; return false; }; m.AttackPlan.prototype.hasSiegeUnits = function() { for (let ent of this.unitCollection.values()) if (m.isSiegeUnit(ent)) return true; return false; }; m.AttackPlan.prototype.hasForceOrder = function(data, value) { for (let ent of this.unitCollection.values()) { if (data && +ent.getMetadata(PlayerID, data) !== value) continue; let orders = ent.unitAIOrderData(); for (let order of orders) if (order.force) return true; } return false; }; /** * The center position of this attack may be in an inaccessible area. So we use the access * of the unit nearest to this center position. */ m.AttackPlan.prototype.getAttackAccess = function(gameState) { for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) return m.getLandAccess(gameState, ent); return 0; }; m.AttackPlan.prototype.debugAttack = function() { API3.warn("---------- attack " + this.name); for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); } API3.warn("------------------------------"); }; m.AttackPlan.prototype.Serialize = function() { let properties = { "name": this.name, "type": this.type, "state": this.state, "forced": this.forced, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "siegeState": this.siegeState, "position5TurnsAgo": this.position5TurnsAgo, "lastPosition": this.lastPosition, "position": this.position, "isBlocked": this.isBlocked, "targetPlayer": this.targetPlayer, "target": this.target !== undefined ? this.target.id() : undefined, "targetPos": this.targetPos, "uniqueTargetId": this.uniqueTargetId, "path": this.path }; return { "properties": properties }; }; m.AttackPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; if (this.target) this.target = gameState.getEntityById(this.target); this.failed = undefined; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 21825) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 21826) @@ -1,1111 +1,1111 @@ var PETRA = function(m) { /** * Base Manager * Handles lower level economic stuffs. * Some tasks: * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. * -giving feedback/estimates on GR * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. * -getting good spots for dropsites * -managing dropsite use in the base * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ m.BaseManager = function(gameState, Config) { this.Config = Config; this.ID = gameState.ai.uniqueIDs.bases++; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; this.anchorId = undefined; this.accessIndex = undefined; // Maximum distance (from any dropsite) to look for resources // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max this.maxDistResourceSquare = 360*360; this.constructing = false; // Defenders to train in this cc when its construction is finished this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; this.timeNextIdleCheck = 0; }; m.BaseManager.prototype.init = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else if (state != "captured") this.neededDefenders = 0; this.workerObject = new m.Worker(this); // entitycollections this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.mobileDropsites = this.units.filter(API3.Filters.isDropsite()); this.units.registerUpdates(); this.workers.registerUpdates(); this.buildings.registerUpdates(); this.mobileDropsites.registerUpdates(); // array of entity IDs, with each being this.dropsites = {}; this.dropsiteSupplies = {}; this.gatherers = {}; for (let res of Resources.GetCodes()) { this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; } }; m.BaseManager.prototype.reset = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else this.constructing = false; if (state != "captured" || this.Config.difficulty < 3) this.neededDefenders = 0; else this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); }; m.BaseManager.prototype.assignEntity = function(gameState, ent) { ent.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(ent); this.workers.updateEnt(ent); this.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) this.assignResourceToDropsite(gameState, ent); }; m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) { if (!anchorEntity.hasClass("CivCentre")) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); else { this.anchor = anchorEntity; this.anchorId = anchorEntity.id(); this.anchor.setMetadata(PlayerID, "baseAnchor", true); gameState.ai.HQ.resetBaseCache(); } anchorEntity.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(anchorEntity); this.accessIndex = m.getLandAccess(gameState, anchorEntity); return true; }; -/* we lost our anchor. Let's reassign our units and buildings */ +/* we lost our anchor. Let's reaffect our units and buildings */ m.BaseManager.prototype.anchorLost = function(gameState, ent) { this.anchor = undefined; this.anchorId = undefined; this.neededDefenders = 0; gameState.ai.HQ.resetBaseCache(); }; /** Set a building of an anchorless base */ m.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) { if (!this.buildings.hasEntities()) { if (!m.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); this.accessIndex = m.getLandAccess(gameState, ent); } else if (this.accessIndex != m.getLandAccess(gameState, ent)) API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + " has been assigned " + ent.templateName() + " with access" + m.getLandAccess(gameState, ent)); ent.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(ent); return true; }; /** * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. * Moving resources (animals) and buildable resources (fields) are treated elsewhere. */ m.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) { if (this.dropsites[dropsite.id()]) { if (this.Config.debug > 0) warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); return; } let accessIndex = this.accessIndex; let dropsitePos = dropsite.position(); let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; if (this.ID == gameState.ai.HQ.baseManagers[0].ID) accessIndex = m.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; for (let type of dropsite.resourceDropsiteTypes()) { let resources = gameState.getResourceSupplies(type); if (!resources.length) continue; let nearby = this.dropsiteSupplies[type].nearby; let medium = this.dropsiteSupplies[type].medium; let faraway = this.dropsiteSupplies[type].faraway; resources.forEach(function(supply) { if (!supply.position()) return; if (supply.hasClass("Animal")) // moving resources are treated differently return; if (supply.hasClass("Field")) // fields are treated separately return; if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately return; // quick accessibility check if (m.getLandAccess(gameState, supply) != accessIndex) return; let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); if (dist < maxDistResourceSquare) { if (dist < maxDistResourceSquare/16) // distmax/4 nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else if (dist < maxDistResourceSquare/4) // distmax/2 medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); } }); nearby.sort((r1, r2) => r1.dist - r2.dist); medium.sort((r1, r2) => r1.dist - r2.dist); faraway.sort((r1, r2) => r1.dist - r2.dist); /* let debug = false; if (debug) { faraway.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); }); medium.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); }); nearby.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); }); } */ } // Allows all allies to use this dropsite except if base anchor to be sure to keep // a minimum of resources for this base Engine.PostCommand(PlayerID, { "type": "set-dropsite-sharing", "entities": [dropsiteId], "shared": dropsiteId != this.anchorId }); }; // completely remove the dropsite resources from our list. m.BaseManager.prototype.removeDropsite = function(gameState, ent) { if (!ent.id()) return; let removeSupply = function(entId, supply){ for (let i = 0; i < supply.length; ++i) { // exhausted resource, remove it from this list if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) supply.splice(i--, 1); // resource assigned to the removed dropsite, remove it else if (supply[i].dropsite == entId) supply.splice(i--, 1); } }; for (let type in this.dropsiteSupplies) { removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); removeSupply(ent.id(), this.dropsiteSupplies[type].medium); removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); } this.dropsites[ent.id()] = undefined; }; /** * Returns the position of the best place to build a new dropsite for the specified resource */ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) { let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. let obstructions = m.createObstructionMap(gameState, this.accessIndex, template); let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray(); let bestIdx; let bestVal = 0; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let cellSize = territoryMap.cellSize; for (let j of this.territoryIndices) { let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) // no room around continue; // we add 3 times the needed resource and once the others (except food) let total = 2*gameState.sharedScript.resourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res != "food") total += gameState.sharedScript.resourceMaps[res].map[j]; total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 if (total <= bestVal) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let dp of dpEnts) { let dpPos = dp.position(); if (!dpPos) continue; let dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 3600) { total = 0; break; } else if (dist < 6400) total *= (Math.sqrt(dist)-60)/20; } if (total <= bestVal) continue; for (let cc of ccEnts) { let ccPos = cc.position(); if (!ccPos) continue; let dist = API3.SquareVectorDistance(ccPos, pos); if (dist < 3600) { total = 0; break; } else if (dist < 6400) total *= (Math.sqrt(dist)-60)/20; } if (total <= bestVal) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = total; bestIdx = i; } if (this.Config.debug > 2) warn(" for dropsite best is " + bestVal); if (bestVal <= 0) return { "quality": bestVal, "pos": [0, 0] }; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return { "quality": bestVal, "pos": [x, z] }; }; m.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) { let count = 0; let check = {}; for (let supply of this.dropsiteSupplies[type].nearby) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; count += supply.ent.resourceSupplyAmount(); } if (nearbyOnly) return count; for (let supply of this.dropsiteSupplies[type].medium) { if (check[supply.id]) continue; check[supply.id] = true; count += 0.6*supply.ent.resourceSupplyAmount(); } return count; }; /** check our resource levels and react accordingly */ m.BaseManager.prototype.checkResourceLevels = function(gameState, queues) { for (let type of Resources.GetCodes()) { if (type == "food") { if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. { let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations let numQueue = queues.field.countQueuedUnits(); // TODO if not yet farms, add a check on time used/lost and build farmstead if needed if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them { if (count < 600) { queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID })); gameState.ai.HQ.needFarm = true; } } else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) { let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; let goal = this.Config.Economy.provisionFields; if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) goal = Math.max(goal-1, 1); if (numFound + numQueue < goal) queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID })); } else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral")) queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID })); continue; } if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral")) { let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted if (count < 900) { queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID })); gameState.ai.HQ.needCorral = true; } } continue; } // Non food stuff if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities()) { this.gatherers[type].nextCheck = gameState.ai.playedTurn; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; continue; } if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) continue; for (let ent of this.gatherersByType(gameState, type).values()) { if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") ++this.gatherers[type].used; else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING") ++this.gatherers[type].lost; } // TODO add also a test on remaining resources. let total = this.gatherers[type].used + this.gatherers[type].lost; if (total > 150 || total > 60 && type != "wood") { let ratio = this.gatherers[type].lost / total; if (ratio > 0.15) { let newDP = this.findBestDropsiteLocation(gameState, type); if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) { // No good dropsite, try to build a new base if no base already planned, // and if not possible, be less strict on dropsite quality. if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && newDP.quality > Math.min(25, 50*0.15/ratio) && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos)); } } this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; } else if (total == 0) this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; } }; /** Adds the estimated gather rates from this base to the currentRates */ m.BaseManager.prototype.addGatherRates = function(gameState, currentRates) { for (let res in currentRates) { // I calculate the exact gathering rate for each unit. // I must then lower that to account for travel time. // Given that the faster you gather, the more travel time matters, // I use some logarithms. // TODO: this should take into account for unit speed and/or distance to target this.gatherersByType(gameState, res).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); if (res == "food") { this.workersBySubrole(gameState, "hunter").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); this.workersBySubrole(gameState, "fisher").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); } } }; m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) { if (!roleless) roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); for (let ent of roleless) { if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat")) ent.setMetadata(PlayerID, "role", "worker"); else if (ent.hasClass("Support") && ent.hasClass("Elephant")) ent.setMetadata(PlayerID, "role", "worker"); } }; /** * If the numbers of workers on the resources is unbalanced then set some of workers to idle so * they can be reassigned by reassignIdleWorkers. * TODO: actually this probably should be in the HQ. */ m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) { this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; // change resource only towards one which is more needed, and if changing will not change this order let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let sumWanted = 0; let sumCurrent = 0; for (let need of mostNeeded) { sumWanted += need.wanted; sumCurrent += need.current; } let scale = 1; if (sumWanted > 0) scale = sumCurrent / sumWanted; for (let i = mostNeeded.length-1; i > 0; --i) { let lessNeed = mostNeeded[i]; for (let j = 0; j < i; ++j) { let moreNeed = mostNeeded[j]; let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; // Ensure that the most wanted resource is not exhausted if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) { if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) continue; // And if so, move the gatherer to the less wanted one. nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); if (nb == 0) return; } // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 // but we require a bit more to avoid too frequent changes if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) { nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); if (nb == 0) return; } } } }; /** * Switch some gatherers (limited to number) from resource "from" to resource "to" * and return remaining number of possible switches. * Prefer FemaleCitizen for food and CitizenSoldier for other resources. */ m.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) { let num = number; let only; let gatherers = this.gatherersByType(gameState, from); if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) only = "CitizenSoldier"; else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) only = "FemaleCitizen"; for (let ent of gatherers.values()) { if (num == 0) return num; if (!ent.canGather(to)) continue; if (only && !ent.hasClass(only)) continue; --num; ent.stopMoving(); ent.setMetadata(PlayerID, "gather-type", to); gameState.ai.HQ.AddTCResGatherer(to); } return num; }; m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) { // Search for idle workers, and tell them to gather resources based on demand if (!idleWorkers) { let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); } for (let ent of idleWorkers) { // Check that the worker isn't garrisoned if (!ent.position()) continue; // Support elephant can only be builders if (ent.hasClass("Support") && ent.hasClass("Elephant")) { ent.setMetadata(PlayerID, "subrole", "idle"); continue; } if (ent.hasClass("Worker")) { // Just emergency repairing here. It is better managed in assignToFoundations if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) ent.repair(this.anchor); else if (ent.isGatherer()) { let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); for (let needed of mostNeeded) { if (!ent.canGather(needed.type)) continue; let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) continue; ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", needed.type); gameState.ai.HQ.AddTCResGatherer(needed.type); break; } } } else if (ent.hasClass("Cavalry")) ent.setMetadata(PlayerID, "subrole", "hunter"); else if (ent.hasClass("FishingBoat")) ent.setMetadata(PlayerID, "subrole", "fisher"); } }; m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); }; m.BaseManager.prototype.gatherersByType = function(gameState, type) { return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); }; /** * returns an entity collection of workers. * They are idled immediatly and their subrole set to idle. */ m.BaseManager.prototype.pickBuilders = function(gameState, workers, number) { let availableWorkers = this.workers.filter(ent => { if (!ent.position() || !ent.isBuilder()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); availableWorkers.sort((a, b) => { let vala = 0; let valb = 0; if (a.getMetadata(PlayerID, "subrole") == "builder") vala = 100; if (b.getMetadata(PlayerID, "subrole") == "builder") valb = 100; if (a.getMetadata(PlayerID, "subrole") == "idle") vala = -50; if (b.getMetadata(PlayerID, "subrole") == "idle") valb = -50; if (a.getMetadata(PlayerID, "plan") === undefined) vala = -20; if (b.getMetadata(PlayerID, "plan") === undefined) valb = -20; return vala - valb; }); let needed = Math.min(number, availableWorkers.length - 3); for (let i = 0; i < needed; ++i) { availableWorkers[i].stopMoving(); availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); workers.addEnt(availableWorkers[i]); } return; }; /** * If we have some foundations, and we don't have enough builder-workers, * try reassigning some other workers who are nearby * AI tries to use builders sensibly, not completely stopping its econ. */ m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); // Check if nothing to build if (!foundations.length && !damagedBuildings.length) return; let workers = this.workers.filter(ent => ent.isBuilder()); let builderWorkers = this.workersBySubrole(gameState, "builder"); let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); // if we're constructing and we have the foundations to our base anchor, only try building that. if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) { foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); let tID = foundations.toEntityArray()[0].id(); workers.forEach(ent => { let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && target != tID) { ent.stopMoving(); ent.setMetadata(PlayerID, "target-foundation", tID); } }); } if (workers.length < 3) { let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); if (fromOtherBase) { let baseID = this.ID; fromOtherBase.forEach(worker => { worker.setMetadata(PlayerID, "base", baseID); worker.setMetadata(PlayerID, "subrole", "builder"); workers.updateEnt(worker); builderWorkers.updateEnt(worker); idleBuilderWorkers.updateEnt(worker); }); } } let builderTot = builderWorkers.length - idleBuilderWorkers.length; // Make the limit on number of builders depends on the available resources let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); let builderRatio = 1; for (let res of Resources.GetCodes()) { if (availableResources[res] < 200) { builderRatio = 0.2; break; } else if (availableResources[res] < 1000) builderRatio = Math.min(builderRatio, availableResources[res] / 1000); } for (let target of foundations.values()) { if (target.hasClass("Field")) continue; // we do not build fields if (gameState.ai.HQ.isNearInvadingArmy(target.position())) if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; // if our territory has shrinked since this foundation was positioned, do not build it if (m.isNotWorthBuilding(gameState, target)) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); if (maxTotalBuilders < 2 && workers.length > 1) maxTotalBuilders = 2; if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && gameState.getPopulationLimit() < gameState.getPopulationMax()) maxTotalBuilders += 2; let targetNB = 2; if (target.hasClass("Fortress") || target.hasClass("Wonder") || target.getMetadata(PlayerID, "phaseUp") == true) targetNB = 7; else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || target.hasClass("Market")) targetNB = 4; else if (target.hasClass("House") || target.hasClass("DropsiteWood")) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { targetNB = 15; maxTotalBuilders = Math.max(maxTotalBuilders, 15); } // if no base yet, everybody should build if (gameState.ai.HQ.numActiveBases() == 0) { targetNB = workers.length; maxTotalBuilders = targetNB; } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); let time = target.buildTime(); nonBuilderWorkers.sort((workerA, workerB) => { let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); // elephant moves slowly, so when far away they are only useful if build time is long if (workerA.hasClass("Elephant")) coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time); else if (workerA.getMetadata(PlayerID, "gather-type") == "food") coeffA *= 3; let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); if (workerB.hasClass("Elephant")) coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time); else if (workerB.getMetadata(PlayerID, "gather-type") == "food") coeffB *= 3; return coeffA - coeffB; }); let current = 0; let nonBuilderTot = nonBuilderWorkers.length; while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) { ++assigned; ++builderTot; let ent = nonBuilderWorkers[current++]; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); } } for (let target of damagedBuildings.values()) { // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building // that's being destroyed. if (gameState.ai.HQ.isNearInvadingArmy(target.position())) { if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; } else if (noRepair && !target.hasClass("CivCentre")) continue; if (target.decaying()) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); let targetNB = 1; if (target.hasClass("Fortress") || target.hasClass("Wonder")) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); targetNB = 5; if (target.healthLevel() < 0.3) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); targetNB = 7; } } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }); let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); nearestNonBuilders.forEach(function(ent) { ++assigned; ++builderTot; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } }; /** Return false when the base is not active (no workers on it) */ m.BaseManager.prototype.update = function(gameState, queues, events) { if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units { // if some active base, reassigns the workers/buildings // otherwise look for anything useful to do, i.e. treasures to gather if (gameState.ai.HQ.numActiveBases() > 0) { for (let ent of this.units.values()) { let bestBase = m.getBestBase(gameState, ent); if (bestBase.ID != this.ID) bestBase.assignEntity(gameState, ent); } for (let ent of this.buildings.values()) { let bestBase = m.getBestBase(gameState, ent); if (!bestBase) { if (ent.hasClass("Dock")) API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); continue; } if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); bestBase.assignEntity(gameState, ent); } } else if (gameState.ai.HQ.canBuildUnits) { this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); } return false; } if (!this.anchor) // This anchor has been destroyed, but the base may still be usable { if (!this.buildings.hasEntities()) { // Reassign all remaining entities to its nearest base for (let ent of this.units.values()) { let base = m.getBestBase(gameState, ent, false, this.ID); base.assignEntity(gameState, ent); } return false; } // If we have a base with anchor on the same land, reassign everything to it let reassignedBase; for (let ent of this.buildings.values()) { if (!ent.position()) continue; let base = m.getBestBase(gameState, ent); if (base.anchor) reassignedBase = base; break; } if (reassignedBase) { for (let ent of this.units.values()) reassignedBase.assignEntity(gameState, ent); for (let ent of this.buildings.values()) { if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); reassignedBase.assignEntity(gameState, ent); } return false; } this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); return true; } Engine.ProfileStart("Base update - base " + this.ID); this.checkResourceLevels(gameState, queues); this.assignToFoundations(gameState); if (this.constructing) { let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); if(owner != 0 && !gameState.isPlayerAlly(owner)) { // we're in enemy territory. If we're too close from the enemy, destroy us. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (cc.owner() != owner) continue; if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) continue; this.anchor.destroy(); gameState.ai.HQ.resetBaseCache(); break; } } } else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) --this.neededDefenders; if (gameState.ai.elapsedTime > this.timeNextIdleCheck && (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); // check if workers can find something useful to do for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); Engine.ProfileStop(); return true; }; m.BaseManager.prototype.Serialize = function() { return { "ID": this.ID, "anchorId": this.anchorId, "accessIndex": this.accessIndex, "maxDistResourceSquare": this.maxDistResourceSquare, "constructing": this.constructing, "gatherers": this.gatherers, "neededDefenders": this.neededDefenders, "territoryIndices": this.territoryIndices, "timeNextIdleCheck": this.timeNextIdleCheck }; }; m.BaseManager.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 21825) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 21826) @@ -1,2898 +1,2898 @@ var PETRA = function(m) { /** * Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: * -defining RESS needs * -BO decisions. * > training workers * > building stuff (though we'll send that to bases) * -picking strategy (specific manager?) * -diplomacy -> diplomacyManager * -planning attacks -> attackManager * -picking new CC locations. */ m.HQ = function(Config) { this.Config = Config; this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache various quantities. this.turnCache = {}; this.lastFailedGather = {}; this.firstBaseConfig = false; this.currentBase = 0; // Only one base (from baseManager) is run every turn. // Workers configuration this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime this.towerStartTime = 0; // stone defense towers, will start as soon as available this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // will start as soon as available this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); this.baseManagers = []; this.attackManager = new m.AttackManager(this.Config); this.buildManager = new m.BuildManager(); this.defenseManager = new m.DefenseManager(this.Config); this.tradeManager = new m.TradeManager(this.Config); this.navalManager = new m.NavalManager(this.Config); this.researchManager = new m.ResearchManager(this.Config); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.garrisonManager = new m.GarrisonManager(this.Config); this.victoryManager = new m.VictoryManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ m.HQ.prototype.init = function(gameState, queues) { this.territoryMap = m.createTerritoryMap(gameState); // initialize base map. Each pixel is a base ID, or 0 if not or not accessible this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = m.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; this.treasures = gameState.getEntities().filter(ent => { let type = ent.resourceSupplyType(); return type && type.generic == "treasure"; }); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ m.HQ.prototype.postinit = function(gameState) { // Rebuild the base maps from the territory indices of each base this.basesMap = new API3.Map(gameState.sharedScript, "territory"); for (let base of this.baseManagers) for (let j of base.territoryIndices) this.basesMap.map[j] = base.ID; for (let ent of gameState.getOwnEntities().values()) { if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) continue; // Entities which have been built or have changed ownership after the last AI turn have no base. // they will be dealt with in the next checkEvents let baseID = ent.getMetadata(PlayerID, "base"); if (baseID === undefined) continue; let base = this.getBaseByID(baseID); base.assignResourceToDropsite(gameState, ent); } this.updateTerritories(gameState); }; /** * Create a new base in the baseManager: * If an existing one without anchor already exist, use it. * Otherwise create a new one. * TODO when buildings, criteria should depend on distance * allowedType: undefined => new base with an anchor * "unconstructed" => new base with a foundation anchor * "captured" => captured base with an anchor * "anchorless" => anchorless base, currently with dock */ m.HQ.prototype.createBase = function(gameState, ent, type) { let access = m.getLandAccess(gameState, ent); let newbase; for (let base of this.baseManagers) { if (base.accessIndex != access) continue; if (type != "anchorless" && base.anchor) continue; if (type != "anchorless") { // TODO we keep the fisrt one, we should rather use the nearest if buildings // and possibly also cut on distance newbase = base; break; } else { // TODO here also test on distance instead of first if (newbase && !base.anchor) continue; newbase = base; if (newbase.anchor) break; } } if (this.Config.debug > 0) { API3.warn(" ----------------------------------------------------------"); API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); } if (!newbase) { newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState, type); this.baseManagers.push(newbase); } else newbase.reset(type); if (type != "anchorless") newbase.setAnchor(gameState, ent); else newbase.setAnchorlessEntity(gameState, ent); return newbase; }; /** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported */ m.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) { let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") return path[1]; if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; }; /** TODO check if the new anchorless bases should be added to addBase */ m.HQ.prototype.checkEvents = function(gameState, events) { let addBase = false; this.buildManager.checkEvents(gameState, events); if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player != PlayerID && evt.otherPlayer != PlayerID) continue; // Reset the entities collections which depend on diplomacy gameState.resetOnDiplomacyChanged(); break; } for (let evt of events.Destroy) { // Let's check we haven't lost an important building here. if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && evt.metadata[PlayerID].base) { let ent = evt.entityObj; if (ent.owner() != PlayerID) continue; // A new base foundation was created and destroyed on the same (AI) turn if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) continue; let base = this.getBaseByID(evt.metadata[PlayerID].base); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) base.anchorLost(gameState, ent); } } for (let evt of events.EntityRenamed) { let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchorId || base.anchorId != evt.entity) continue; base.anchorId = evt.newentity; base.anchor = ent; } for (let evt of events.Create) { // Let's check if we have a valuable foundation needing builders quickly // (normal foundations are taken care in baseManager.assignToFoundations) let ent = gameState.getEntityById(evt.entity); if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) continue; if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc { // Okay so let's try to create a new base around this. let newbase = this.createBase(gameState, ent, "unconstructed"); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 10); if (builders !== false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock { let newbase = this.createBase(gameState, ent, "anchorless"); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 4); if (builders != false) { builders.forEach(worker => { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (let evt of events.ConstructionFinished) { if (evt.newentity == evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.hasClass("BarterMarket") && this.maxFields) this.maxFields = false; if (ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes()) base.assignResourceToDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) { if (base.constructing) base.constructing = false; addBase = true; } } for (let evt of events.OwnershipChanged) // capture events { if (evt.from == PlayerID) { let ent = gameState.getEntityById(evt.entity); if (!ent || ent.getMetadata(PlayerID, "base") === undefined) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) base.removeDropsite(gameState, ent); if (ent.getMetadata(PlayerID, "baseAnchor") === true) base.anchorLost(gameState, ent); } if (evt.to != PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.hasClass("Unit")) { m.getBestBase(gameState, ent).assignEntity(gameState, ent); ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.hasClass("Trader")) { ent.setMetadata(PlayerID, "role", "trader"); ent.setMetadata(PlayerID, "route", undefined); } if (ent.hasClass("Worker")) { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", "idle"); } if (ent.hasClass("Ship")) m.setSeaAccess(gameState, ent); if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase; if (ent.foundationProgress() !== undefined) newbase = this.createBase(gameState, ent, "unconstructed"); else { newbase = this.createBase(gameState, ent, "captured"); addBase = true; } newbase.assignEntity(gameState, ent); } else { let base; // If dropsite on new island, create a base around it if (!ent.decaying() && ent.resourceDropsiteTypes()) base = this.createBase(gameState, ent, "anchorless"); else base = m.getBestBase(gameState, ent) || this.baseManagers[0]; base.assignEntity(gameState, ent); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } } } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events.TrainingStarted) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events.TrainingFinished) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holderId = ent.unitAIOrderData()[0].target; let holder = gameState.getEntityById(holderId); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state != "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } // Assign it immediately to something useful to do if (ent.getMetadata(PlayerID, "role") == "worker") { let base; if (ent.getMetadata(PlayerID, "base") === undefined) { base = m.getBestBase(gameState, ent); base.assignEntity(gameState, ent); } else base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.reassignIdleWorkers(gameState, [ent]); base.workerObject.update(gameState, ent); } else if (ent.resourceSupplyType() && ent.position()) { let type = ent.resourceSupplyType(); if (!type.generic) continue; let dropsites = gameState.getOwnDropsites(type.generic); let pos = ent.position(); let access = m.getLandAccess(gameState, ent); let distmin = Math.min(); let goal; for (let dropsite of dropsites.values()) { if (!dropsite.position() || m.getLandAccess(gameState, dropsite) != access) continue; let dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist > distmin) continue; distmin = dist; goal = dropsite.position(); } if (goal) ent.moveToRange(goal[0], goal[1]); } } } for (let evt of events.TerritoryDecayChanged) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } if (addBase) { if (!this.firstBaseConfig) { // This is our first base, let us configure our starting resources this.configFirstBase(gameState); } else { // Let us hope this new base will fix our possible resource shortage this.saveResources = undefined; this.saveSpace = undefined; this.maxFields = false; } } // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < 2) return; for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.70 + randFloat(0., 0.1); for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.85 + randFloat(0., 0.1); break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; /** Ensure that all requirements are met when phasing up*/ m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { if (gameState.getNumberOfPhases() == this.currentPhase) return; let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); let plan; let queue; for (let entityReq of requirements) { // Village requirements are met elsewhere by constructing more houses if (entityReq.class == "Village" || entityReq.class == "NotField") continue; if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) continue; switch (entityReq.class) { case "Town": if (!queues.economicBuilding.hasQueuedUnits() && !queues.militaryBuilding.hasQueuedUnits() && !queues.defenseBuilding.hasQueuedUnits()) { if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_market")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_market", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_temple")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_blacksmith")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "phaseUp": true }); queue = "militaryBuilding"; break; } if (this.canBuild(gameState, "structures/{civ}_defense_tower")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower", { "phaseUp": true }); queue = "defenseBuilding"; break; } } break; default: // All classes not dealt with inside vanilla game. // We put them for the time being on the economic queue, except if wonder queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new m.ConstructionPlan(gameState, structure, { "phaseUp": true }); } } if (plan) { if (queue == "wonder") { gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); plan.queueToReset = "majorTech"; } else { gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); plan.queueToReset = queue; } queues[queue].addPlan(plan); return; } } }; /** Called by any "phase" research plan once it's started */ m.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [ ["costsResource", 1, "food"] ]; let classesDef = ["Support", "Worker"]; let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); // counting the workers that aren't part of a plan let numberOfWorkers = 0; // all workers let numberOfSupports = 0; // only support workers (i.e. non fighting) gameState.getOwnUnits().forEach(ent => { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) { ++numberOfWorkers; if (ent.hasClass("Support")) ++numberOfSupports; } }); let numberInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { numberInTraining += item.count; if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan === undefined) { numberOfWorkers += item.count; if (item.metadata.support) numberOfSupports += item.count; } } }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); if (queues.villager.plans[0]) { queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); if (queues.villager.plans[1]) queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); } if (queues.citizenSoldier.plans[0]) { queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); if (queues.citizenSoldier.plans[1]) queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); } let numberOfQueuedSupports = queues.villager.countQueuedUnits(); let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; let numberTotal = numberOfWorkers + numberQueued; if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers // and gives a ratio alfa at startup. let supportRatio = this.supportRatio; let alpha = 0.85; if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field"))) supportRatio = Math.min(this.supportRatio, 0.1); if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) alpha = 0.7; if (gameState.isCeasefireActive()) alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); let template; if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; else requirements = [ ["strength", 1] ]; let classes = ["CitizenSoldier", "Infantry"]; // We want at least 33% ranged and 33% melee classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); template = this.findBestTrainableUnit(gameState, classes, requirements); } // If the template variable is empty, the default unit (Support unit) will be used // base "0" means automatic choice of base if (!template && templateDef) queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { let units; if (classes.indexOf("Hero") != -1) units = gameState.findTrainableUnits(classes, []); else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it units = gameState.findTrainableUnits(classes, ["SiegeTower"]); else // We do not want hero when not explicitely specified units = gameState.findTrainableUnits(classes, ["Hero"]); if (!units.length) return undefined; let parameters = requirements.slice(); let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (let type in remainingResources) { if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; let toAdd = true; for (let param of parameters) { if (param[0] != "costsResource" || param[2] != type) continue; param[1] = Math.min(param[1], costsResource); toAdd = false; break; } if (toAdd) parameters.push(["costsResource", costsResource, type]); } units.sort((a, b) => { let aCost = 1 + a[1].costSum(); let bCost = 1 + b[1].costSum(); let aValue = 0.1; let bValue = 0.1; for (let param of parameters) { if (param[0] == "strength") { aValue += m.getMaxStrength(a[1]) * param[1]; bValue += m.getMaxStrength(b[1]) * param[1]; } else if (param[0] == "siegeStrength") { aValue += m.getMaxStrength(a[1], "Structure") * param[1]; bValue += m.getMaxStrength(b[1], "Structure") * param[1]; } else if (param[0] == "speed") { aValue += a[1].walkSpeed() * param[1]; bValue += b[1].walkSpeed() * param[1]; } else if (param[0] == "costsResource") { // requires a third parameter which is the resource if (a[1].cost()[param[2]]) aValue *= param[1]; if (b[1].cost()[param[2]]) bValue *= param[1]; } else if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aValue *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bValue *= param[1]; } else API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); } return -aValue/aCost + bValue/bCost; }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { let accessIndex = baseRef.accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. let baseBest = this.baseManagers.slice().sort((a, b) => { if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) return -1; else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) return 1; return 0; }); let needed = number; let workers = new API3.EntityCollection(gameState.sharedScript); for (let base of baseBest) { if (base.ID == baseRef.ID) continue; base.pickBuilders(gameState, workers, needed); if (workers.length >= number) break; needed = number - workers.length; } if (!workers.length) return false; return workers; }; m.HQ.prototype.getTotalResourceLevel = function(gameState) { let total = {}; for (let res of Resources.GetCodes()) total[res] = 0; for (let base of this.baseManagers) for (let res in total) total[res] += base.getResourceLevel(gameState, res); return total; }; /** * Returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ m.HQ.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache.currentRates) { let currentRates = {}; for (let res of Resources.GetCodes()) currentRates[res] = 0.5 * this.GetTCResGatherer(res); for (let base of this.baseManagers) base.addGatherRates(gameState, currentRates); for (let res of Resources.GetCodes()) currentRates[res] = Math.max(currentRates[res], 0); this.turnCache.currentRates = currentRates; } return this.turnCache.currentRates; }; /** * Returns the wanted gather rate. */ m.HQ.prototype.GetWantedGatherRates = function(gameState) { if (!this.turnCache.wantedRates) this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); return this.turnCache.wantedRates; }; /** * Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ m.HQ.prototype.pickMostNeededResources = function(gameState) { let wantedRates = this.GetWantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); let needed = []; for (let res in wantedRates) needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { if (a.current < a.wanted && b.current < b.wanted) { if (a.current && b.current) return b.wanted / b.current - a.wanted / a.current; if (a.current) return 1; if (b.current) return -1; return b.wanted - a.wanted; } if (a.current < a.wanted || a.wanted && !b.wanted) return -1; if (b.current < b.wanted || b.wanted && !a.wanted) return 1; return a.current - a.wanted - b.current + b.wanted; }); return needed; }; /** * Returns the best position to build a new Civil Centre * Whose primary function would be to reach new resources of type "resource". */ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then look for a good spot. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); let dpList = []; for (let dp of dpEnts.values()) dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let scale = 250 * 250; let proxyAccess; let nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; let nearbyRejected = Math.square(120); // Reject if too near from any cc let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // With enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // We require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips == 0 && proxyAccess != index) continue; let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // Checking distance to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // We will be more tolerant for cc around our oversea docks let oversea = false; if (proximity) // This is our first cc, let's do it near our units norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale; else { let minDist = Math.min(); let accessible = false; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < nearbyRejected) { norm = 0; break; } if (!cc.ally) continue; if (dist < nearbyAllyRejected) { norm = 0; break; } if (dist < nearbyAllyDisfavored) norm *= 0.5; if (dist < minDist) minDist = dist; accessible = accessible || index == m.getLandAccess(gameState, cc.ent); } if (norm == 0) continue; if (accessible && minDist > maxAccessRejected) continue; if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc { if (!accessible) { if (minDist > maxNoAccessDisfavored) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } // Not near any of our dropsite, except for oversea docks oversea = !accessible && dpList.some(dp => m.getLandAccess(gameState, dp.ent) == index); if (!oversea) { for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } } if (norm == 0) continue; } if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map norm *= 0.5; let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res != "food") val += gameState.sharedScript.ccResourceMaps[res].map[j]; val *= norm; // If oversea, be just above threshold to be accepted if nothing else if (oversea) val = Math.max(val, cut + 0.1); if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); if (bestVal === undefined) return false; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new Civil Centre * Whose primary function would be to assure territorial continuity with our allies */ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let ccList = []; let numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({ "pos": cc.position(), "ally": ally }); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; let minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - favoredDistance; currentVal = delta*delta; delta = Math.sqrt(distcc1) - favoredDistance; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - favoredDistance; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & m.fullBorder_Mask) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new market: if the allies already have a market, build it as far as possible * from it, although not in our border to be able to defend it easily. If no allied market, our second market will * follow the same logic. * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance * for performance reasons. */ m.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let bestDistSq; let bestGainMult; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let isNavalMarket = template.hasClass("NavalMarket"); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let j = 0; j < this.territoryMap.length; ++j) { // do not try on the narrow border of our territory if (this.borderMap.map[j] & m.narrowFrontier_Mask) continue; if (this.basesMap.map[j] == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets let maxVal = 0; let maxDistSq; let maxGainMult; let gainMultiplier; for (let market of markets) { if (isNavalMarket && market.hasClass("NavalMarket")) { if (m.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (m.getLandAccess(gameState, market) == index && !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let distSq = API3.SquareVectorDistance(market.position(), pos); if (gainMultiplier * distSq > maxVal) { maxVal = gainMultiplier * distSq; maxDistSq = distSq; maxGainMult = gainMultiplier; } } if (maxVal == 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; bestDistSq = maxDistSq; bestGainMult = maxGainMult; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // do not keep it if gain is too small, except if this is our first BarterMarket let idx; if (expectedGain < this.tradeManager.minimalGain) { if (template.hasClass("BarterMarket") && !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) idx = -1; // needed by queueplanBuilding manager to keep that market else return false; } else idx = this.basesMap.map[bestJdx]; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, idx, expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ m.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals { enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getVictoryConditions().has("wonder"); let wonderDistmin; let wonders; if (wonderMode) { wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = wonders.length != 0; if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let isTower = template.hasClass("Tower"); let isFortress = template.hasClass("Fortress"); let radius; if (isFortress) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (!(this.borderMap.map[j] & m.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower) continue; } if (this.basesMap.map[j] == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures let minDist = Math.min(); let dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80*80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; let cutDist = 900; // 30*30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0 || minDist == Math.min()) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx]]; }; m.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) return; let templateName = "structures/{civ}_temple"; if (this.canBuild(gameState, "structures/{civ}_temple_vesta")) templateName = "structures/{civ}_temple_vesta"; else if (!this.canBuild(gameState, templateName)) return; queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, templateName)); }; m.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}_market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) { if (!queues.economicBuilding.paused) { // Put available resources in this market let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.plans[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.canAfford("economicBuilding", cost)) { for (let q in queueManager.queues) { if (q == "economicBuilding") continue; queueManager.transferAccounts(cost, q, "economicBuilding"); if (queueManager.canAfford("economicBuilding", cost)) break; } } } return; } gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ m.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) return; // Wait to have at least one dropsite and house before the farmstead if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) return; if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) return; if (!this.canBuild(gameState, "structures/{civ}_farmstead")) return; queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); }; /** * Try to build a wonder when required * force = true when called from the victoryManager in case of Wonder victory condition. */ m.HQ.prototype.buildWonder = function(gameState, queues, force = false) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}_wonder")) return; if (!force) { let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_wonder")); // Check that we have enough resources to start thinking to build a wonder let cost = template.cost(); let resources = gameState.getResources(); let highLevel = 0; let lowLevel = 0; for (let res in cost) { if (resources[res] && resources[res] > 0.7 * cost[res]) ++highLevel; else if (!resources[res] || resources[res] < 0.3 * cost[res]) ++lowLevel; } if (highLevel == 0 || lowLevel > 1) return; } queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); }; /** Build a corral, and train animals there */ m.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) && nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) { if (this.canBuild(gameState, "structures/{civ}_corral")) { queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); return; } if (!nCorral) return; } // And train some animals let civ = gameState.getPlayerCiv(); for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.isHuntable()) continue; let count = gameState.countEntitiesByType(trainable, true); for (let item of corral.trainingQueue()) count += item.count; if (count > nCorral) continue; queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); return; } } }; /** * build more houses if needed. * kinda ugly, lots of special cases to both build enough houses but not tooo many… */ m.HQ.prototype.buildMoreHouses = function(gameState, queues) { if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) || gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) { if (!houseTemplate.hasClass(entityReq.class)) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) if (houseQueue[i].isGo(gameState)) --needed; else if (needed > 0) { houseQueue[i].goRequirement = undefined; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses let house = gameState.applyCiv("structures/{civ}_house"); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); let priority; if (freeSlots < 5) { if (this.buildManager.isUnbuildable(gameState, house)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2*this.Config.priorities.house; } else priority = this.Config.priorities.house; if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; // First build one cc if all have been destroyed if (this.numPotentialBases() == 0) { this.buildFirstBase(gameState); return; } // Then expand if we have not enough room available for buildings if (this.buildManager.numberMissingRoom(gameState) > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } // If we've already planned to phase up, wait a bit before trying to expand if (this.phasing) return; // Finally expand if we have lots of units (threshold depending on the aggressivity value) let activeBases = this.numActiveBases(); let numUnits = gameState.getOwnUnits().length; let numvar = 10 * (1 - this.Config.personality.aggressive); if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template; // We require at least one of this civ civCentre as they may allow specific units or techs let hasOwnCC = false; for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) { if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}_civil_centre")) continue; hasOwnCC = true; break; } if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) template = "structures/{civ}_military_colony"; else if (this.canBuild(gameState, "structures/{civ}_civil_centre")) template = "structures/{civ}_civil_centre"; else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) template = "structures/{civ}_military_colony"; else return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ m.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { // try to build fortresses if (this.canBuild(gameState, "structures/{civ}_fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && numFortresses < this.numActiveBases() + 1 + this.extraFortresses && numFortresses < Math.floor(gameState.getPopulation() / 25) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (!numFortresses) gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) { let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime; if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); } return; } if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && numTowers < Math.floor(gameState.getPopulation() / 8) && gameState.getOwnFoundationsByClass("DefenseTower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBases() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; m.HQ.prototype.buildBlacksmith = function(gameState, queues) { if (this.getAccountedPopulation(gameState) < this.Config.Military.popForBlacksmith || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) return; // build a market before the blacksmith if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}_blacksmith")) queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); }; /** * Deals with constructing military buildings (barracks, stables…) * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && numBarracks != 0) return; let barracksTemplate = this.canBuild(gameState, "structures/{civ}_barracks") ? "structures/{civ}_barracks" : undefined; let rangeTemplate = this.canBuild(gameState, "structures/{civ}_range") ? "structures/{civ}_range" : undefined; let numRanges = gameState.getOwnEntitiesByClass("Archery", true).length; numBarracks -= numRanges; let stableTemplate = this.canBuild(gameState, "structures/{civ}_stables") ? "structures/{civ}_stables" : this.canBuild(gameState, "structures/{civ}_stable") ? "structures/{civ}_stable" : undefined; let numStables = gameState.getOwnEntitiesByClass("Stables", true).length; if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) { // first barracks/range and stables. if (numBarracks + numRanges == 0) { let template = barracksTemplate || rangeTemplate; if (template) { gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); let plan = new m.ConstructionPlan(gameState, template, { "militaryBase": true }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } } if (numStables == 0 && stableTemplate) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Second range/barracks and stables if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); if (template) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Then 3rd barracks/range/stables if needed if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) { let template = barracksTemplate || stableTemplate || rangeTemplate; if (template) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } } if (this.saveResources) return; if (this.currentPhase < 3) return; if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities()) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true })); return; } if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities()) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true })); return; } if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) return; // Build advanced military buildings let nAdvanced = 0; for (let advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) { for (let advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) continue; let template = gameState.getTemplate(advanced); if (!template) continue; let civ = gameState.getPlayerCiv(); if (template.hasDefensiveFire() || template.trainableEntities(civ)) queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true })); else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); return; } } }; /** * Find base nearest to ennemies for military buildings. */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase; let enemyFound = false; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) continue; let access = m.getLandAccess(gameState, cce); let isEnemy = gameState.isPlayerEnemy(cce.owner()); for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; if (m.getLandAccess(gameState, cc) != access) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (!enemyFound && isEnemy) enemyFound = true; else if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil centre from a given set of positions * and garrison them there for defense */ m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.hasQueuedUnits()) return false; let civ = gameState.getPlayerCiv(); // find nearest base anchor let distcut = 20000; let nearestAnchor; let distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (m.getLandAccess(gameState, base.anchor) != access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || item.metadata && item.metadata.garrisonType) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata.garrisonType) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); let rangedWanted = randBool() && autogarrison; let total = gameState.getResources(); let templateFound; let trainables = nearestAnchor.trainableEntities(civ); let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) continue; if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") == rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.canAfford("emergency", cost)) { for (let q in queueManager.queues) { if (q == "emergency") continue; queueManager.transferAccounts(cost, q, "emergency"); if (queueManager.canAfford("emergency", cost)) break; } } let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = "protection"; gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; m.HQ.prototype.canBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.buildManager.isUnbuildable(gameState, type)) return false; if (gameState.isTemplateDisabled(type)) { this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) { this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); return false; } if (!template.available(gameState)) { this.buildManager.setUnbuildable(gameState, type, 30, "tech"); return false; } if (!this.buildManager.hasBuilder(type)) { this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } if (this.numActiveBases() < 1) { // if no base, check that we can build outside our territory let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) { this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; m.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; let alliedVictory = gameState.getAlliedVictory(); let passabilityMap = gameState.getPassabilityMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let insideSmall = Math.round(45 / cellSize); let insideLarge = Math.round(80 / cellSize); // should be about the range of towers let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] & m.outside_Mask) continue; if (this.borderMap.map[j] & m.fullFrontier_Mask) this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) { // If this tile was already accounted, remove it if (this.basesMap.map[j] == 0) continue; let base = this.getBaseByID(this.basesMap.map[j]); if (base) { let index = base.territoryIndices.indexOf(j); if (index != -1) base.territoryIndices.splice(index, 1); else API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); } else API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); this.basesMap.map[j] = 0; } else { // Update the frontier let ix = j%width; let iz = Math.floor(j/width); let onFrontier = false; for (let a of around) { let jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & m.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= m.narrowFrontier_Mask; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & m.outside_Mask) continue; territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) onFrontier = true; } if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask)) this.borderMap.map[j] |= m.largeFrontier_Mask; // If this tile was not already accounted, add it. if (this.basesMap.map[j] != 0) continue; let landPassable = false; let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); let access; for (let k of ind) { if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) continue; landPassable = true; access = gameState.ai.accessibility.landPassMap[k]; break; } if (!landPassable) continue; let distmin = Math.min(); let baseID; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (!baseID) continue; this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; expansion++; } } if (!expansion) return; // We've increased our territory, so we may have some new room to build this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; /** Reassign territories when a base is going to be deleted */ m.HQ.prototype.reassignTerritories = function(deletedBase) { let cellSize = this.territoryMap.cellSize; let width = this.territoryMap.width; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.basesMap.map[j] != deletedBase.ID) continue; if (this.territoryMap.getOwnerIndex(j) != PlayerID) { API3.warn("Petra reassignTerritories: should never happen"); this.basesMap.map[j] = 0; continue; } let distmin = Math.min(); let baseID; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != deletedBase.accessIndex) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (baseID) { this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; } else this.basesMap.map[j] = 0; } }; /** * returns the base corresponding to baseID */ m.HQ.prototype.getBaseByID = function(baseID) { for (let base of this.baseManagers) if (base.ID == baseID) return base; return undefined; }; /** * returns the number of bases with a cc * ActiveBases includes only those with a built cc * PotentialBases includes also those with a cc in construction */ m.HQ.prototype.numActiveBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.active; }; m.HQ.prototype.numPotentialBases = function() { if (!this.turnCache.base) this.updateBaseCache(); return this.turnCache.base.potential; }; m.HQ.prototype.updateBaseCache = function() { this.turnCache.base = { "active": 0, "potential": 0 }; for (let base of this.baseManagers) { if (!base.anchor) continue; ++this.turnCache.base.potential; if (base.anchor.foundationProgress() === undefined) ++this.turnCache.base.active; } }; m.HQ.prototype.resetBaseCache = function() { this.turnCache.base = undefined; }; /** * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). + * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). */ m.HQ.prototype.assignGatherers = function() { for (let base of this.baseManagers) { for (let worker of base.workers.values()) { if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE") continue; let orders = worker.unitAIOrderData(); if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) continue; this.AddTCGatherer(orders[1].target); } } }; m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; /** Check that the chosen position is not too near from an invading army */ m.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache.firingStructures.values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; /** Compute the capture strength of all units attacking a capturable target */ m.HQ.prototype.updateCaptureStrength = function(gameState) { this.capturableTargets.clear(); for (let ent of gameState.getOwnUnits().values()) { if (!ent.canCapture()) continue; let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || !target.isCapturable() || !ent.canCapture(target)) continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { "strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturableTarget.ents.add(ent.id()); } } for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); let allowCapture; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); if (allowCapture === undefined) allowCapture = m.allowCapture(gameState, ent, target); let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].attackType) continue; if ((orderData[0].attackType == "Capture") !== allowCapture) ent.attack(targetId, allowCapture); } } this.capturableTargetsTime = gameState.ai.elapsedTime; }; /** Some functions that register that we assigned a gatherer to a resource this turn */ /** add a gatherer to the turn cache for this supply. */ m.HQ.prototype.AddTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) ++this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = 1; } }; /** remove a gatherer to the turn cache for this supply. */ m.HQ.prototype.RemoveTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) --this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = -1; } }; m.HQ.prototype.GetTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) return this.turnCache.resourceGatherer[supplyID]; return 0; }; /** The next two are to register that we assigned a gatherer to a resource this turn. */ m.HQ.prototype.AddTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) ++this.turnCache["resourceGatherer-" + resource]; else this.turnCache["resourceGatherer-" + resource] = 1; if (this.turnCache.currentRates) this.turnCache.currentRates[resource] += 0.5; }; m.HQ.prototype.GetTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) return this.turnCache["resourceGatherer-" + resource]; return 0; }; /** * flag a resource as exhausted */ m.HQ.prototype.isResourceExhausted = function(resource) { if (this.turnCache["exhausted-" + resource] == undefined) this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => !base.dropsiteSupplies[resource].nearby.length && !base.dropsiteSupplies[resource].medium.length && !base.dropsiteSupplies[resource].faraway.length); return this.turnCache["exhausted-" + resource]; }; /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ m.HQ.prototype.isDefendable = function(ent) { if (!this.turnCache.numAround) this.turnCache.numAround = {}; if (this.turnCache.numAround[ent.id()] === undefined) this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); return +this.turnCache.numAround[ent.id()] > 8; }; /** * Get the number of population already accounted for */ m.HQ.prototype.getAccountedPopulation = function(gameState) { if (this.turnCache.accountedPopulation == undefined) { let pop = gameState.getPopulation(); for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.unitTemplate) continue; let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); if (unitPop) pop += item.count * unitPop; } } this.turnCache.accountedPopulation = pop; } return this.turnCache.accountedPopulation; }; /** * Get the number of workers already accounted for */ m.HQ.prototype.getAccountedWorkers = function(gameState) { if (this.turnCache.accountedWorkers == undefined) { let workers = gameState.getOwnEntitiesByRole("worker", true).length; for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") continue; workers += item.count; } } this.turnCache.accountedWorkers = workers; } return this.turnCache.accountedWorkers; }; /** * Some functions are run every turn * Others once in a while */ m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = m.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities(); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { if (this.Config.debug > 0) API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + " phasing " + this.phasing); this.currentPhase = gameState.currentPhase(); // In principle, this.phasing should be already reset to 0 when starting the research // but this does not work in case of an autoResearch tech if (this.phasing) this.phasing = 0; } /* if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; m.dumpEntity(ent); }); } */ this.checkEvents(gameState, events); this.navalManager.checkEvents(gameState, queues, events); if (this.phasing) this.checkPhaseRequirements(gameState, queues); else this.researchManager.checkPhase(gameState, queues); if (this.numActiveBases() > 0) { if (gameState.ai.playedTurn % 4 == 0) this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 4 == 1) this.buildMoreHouses(gameState, queues); if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) this.buildFarmstead(gameState, queues); if (this.needCorral && gameState.ai.playedTurn % 4 == 3) this.manageCorral(gameState, queues); if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (this.numPotentialBases() < 1 || this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } if (gameState.ai.playedTurn % 30 == 0 && gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) this.buildWonder(gameState, queues, false); } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (gameState.ai.playedTurn % 3 == 0) { this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); } this.assignGatherers(); let nbBases = this.baseManagers.length; let activeBase; // We will loop only on 1 active base per turn do { this.currentBase %= this.baseManagers.length; activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); --nbBases; // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); } while (!activeBase && nbBases != 0); this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.victoryManager.update(gameState, events, queues); // We update the capture strength at the end as it can change attack orders if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) this.updateCaptureStrength(gameState); Engine.ProfileStop(); }; m.HQ.prototype.Serialize = function() { let properties = { "phasing": this.phasing, "currentBase": this.currentBase, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "maxFields": this.maxFields, "canExpand": this.canExpand, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures, "capturableTargets": this.capturableTargets, "capturableTargetsTime": this.capturableTargetsTime }; let baseManagers = []; for (let base of this.baseManagers) baseManagers.push(base.Serialize()); if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); API3.warn(" baseManagers " + uneval(baseManagers)); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), }; }; m.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.baseManagers = []; for (let base of data.baseManagers) { // the first call to deserialize set the ID base needed by entitycollections let newbase = new m.BaseManager(gameState, this.Config); newbase.Deserialize(gameState, base); newbase.init(gameState); newbase.Deserialize(gameState, base); this.baseManagers.push(newbase); } this.navalManager = new m.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new m.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.buildManager = new m.BuildManager(); this.buildManager.Deserialize(data.buildManager); this.defenseManager = new m.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new m.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new m.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new m.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.victoryManager = new m.VictoryManager(this.Config); this.victoryManager.Deserialize(data.victoryManager); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 21825) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 21826) @@ -1,723 +1,723 @@ var PETRA = function(m) { /** * Manage the trade */ m.TradeManager = function(Config) { this.Config = Config; this.tradeRoute = undefined; this.potentialTradeRoute = undefined; this.routeProspection = false; this.targetNumTraders = this.Config.Economy.targetNumTraders; this.warnedAllies = {}; }; m.TradeManager.prototype.init = function(gameState) { this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); this.traders.registerUpdates(); this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5; }; m.TradeManager.prototype.hasTradeRoute = function() { return this.tradeRoute !== undefined; }; m.TradeManager.prototype.assignTrader = function(ent) { ent.setMetadata(PlayerID, "role", "trader"); this.traders.updateEnt(ent); }; m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) { if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) return; let numTraders = this.traders.length; let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length; let numLandTraders = numTraders - numSeaTraders; // add traders already in training gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role != "trader") continue; numTraders += item.count; if (item.metadata.sea !== undefined) numSeaTraders += item.count; else numLandTraders += item.count; } }); if (numTraders >= this.targetNumTraders && (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) return; let template; let metadata = { "role": "trader" }; if (this.tradeRoute.sea) { - // if we have some merchand ships assigned to transport, try first to reassign them + // if we have some merchand ships affected to transport, try first to reaffect them // May-be, there were produced at an early stage when no other ship were available // and the naval manager will train now more appropriate ships. let already = false; let shipToSwitch; gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) { if (already || !ship.hasClass("Trader")) return; if (ship.getMetadata(PlayerID, "role") == "switchToTrader") { already = true; return; } shipToSwitch = ship; }); if (already) return; if (shipToSwitch) { if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) shipToSwitch.setMetadata(PlayerID, "role", "trader"); else shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader"); return; } template = gameState.applyCiv("units/{civ}_ship_merchant"); metadata.sea = this.tradeRoute.sea; } else { template = gameState.applyCiv("units/{civ}_support_trader"); if (!this.tradeRoute.source.hasClass("NavalMarket")) metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base"); else metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base"); } if (!gameState.getTemplate(template)) { if (this.Config.debug > 0) API3.warn("Petra error: trying to train " + template + " for civ " + gameState.getPlayerCiv() + " but no template found."); return; } queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1)); }; m.TradeManager.prototype.updateTrader = function(gameState, ent) { if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 && !ent.unitAIState().startsWith("INDIVIDUAL.GATHER") && m.gatherTreasure(gameState, ent, true)) return; if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) return; if (ent.getMetadata(PlayerID, "transport") !== undefined) return; // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain Engine.ProfileStart("Trade Manager"); let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); let route = this.checkRoutes(gameState, access); if (!route) { // TODO try to garrison land trader inside merchant ship when only sea routes available if (this.Config.debug > 0) API3.warn(" no available route for " + ent.genericName() + " " + ent.id()); Engine.ProfileStop(); return; } let nearerSource = true; if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position())) nearerSource = false; if (!ent.hasClass("Ship") && route.land != access) { if (nearerSource) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position()); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position()); Engine.ProfileStop(); return; } if (nearerSource) ent.tradeRoute(route.target, route.source); else ent.tradeRoute(route.source, route.target); ent.setMetadata(PlayerID, "route", this.routeEntToId(route)); Engine.ProfileStop(); }; m.TradeManager.prototype.setTradingGoods = function(gameState) { let tradingGoods = {}; for (let res of Resources.GetCodes()) tradingGoods[res] = 0; // first, try to anticipate future needs let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); let remaining = 100; let targetNum = this.Config.Economy.targetNumTraders; for (let res in stocks) { if (res == "food") continue; let wantedRate = wantedRates[res]; if (stocks[res] < 200) { tradingGoods[res] = wantedRate > 0 ? 20 : 10; targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); } else if (stocks[res] < 500) { tradingGoods[res] = wantedRate > 0 ? 15 : 10; targetNum += 2; } else if (stocks[res] < 1000) { tradingGoods[res] = 10; targetNum += 1; } remaining -= tradingGoods[res]; } this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); // then add what is needed now let mainNeed = Math.floor(remaining * 70 / 100); let nextNeed = remaining - mainNeed; tradingGoods[mostNeeded[0].type] += mainNeed; if (mostNeeded[1].wanted > 0) tradingGoods[mostNeeded[1].type] += nextNeed; else tradingGoods[mostNeeded[0].type] += nextNeed; Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods }); if (this.Config.debug > 2) API3.warn(" trading goods set to " + uneval(tradingGoods)); }; /** * Try to barter unneeded resources for needed resources. * only once per turn because the info is not updated within a turn */ m.TradeManager.prototype.performBarter = function(gameState) { let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); if (barterers.length == 0) return false; // Available resources after account substraction let available = gameState.ai.queueManager.getAvailableResources(gameState); let needs = gameState.ai.queueManager.currentNeeds(gameState); let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); let barterPrices = gameState.getBarterPrices(); // calculates conversion rates let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); // loop through each missing resource checking if we could barter and help finishing a queue quickly. for (let buy of Resources.GetCodes()) { // Check if our rate allows to gather it fast enough if (needs[buy] == 0 || needs[buy] < rates[buy] * 30) continue; // Pick the best resource to barter. let bestToSell; let bestRate = 0; for (let sell of Resources.GetCodes()) { if (sell == buy) continue; // Do not sell if we need it or do not have enough buffer if (needs[sell] > 0 || available[sell] < 500) continue; let barterRateMin; if (sell == "food") { barterRateMin = 30; if (available[sell] > 40000) barterRateMin = 0; else if (available[sell] > 15000) barterRateMin = 5; else if (available[sell] > 1000) barterRateMin = 10; } else { barterRateMin = 70; if (available[sell] > 5000) barterRateMin = 30; else if (available[sell] > 1000) barterRateMin = 50; if (buy == "food") barterRateMin += 20; } let barterRate = getBarterRate(barterPrices, buy, sell); if (barterRate > bestRate && barterRate > barterRateMin) { bestRate = barterRate; bestToSell = sell; } } if (bestToSell !== undefined) { let amount = available[bestToSell] > 5000 ? 500 : 100; barterers[0].barter(buy, bestToSell, amount); if (this.Config.debug > 2) API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + " rate buy " + rates[buy] + " available sell " + available[bestToSell] + " available buy " + available[buy] + " barterRate " + bestRate + " amount " + amount); return true; } } // now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices) if (available.food < 1000 || needs.food > 0) return false; let bestToBuy; let bestChoice = 0; for (let buy of Resources.GetCodes()) { if (buy == "food") continue; let barterRateMin = 80; if (available[buy] < 5000 && available.food > 5000) barterRateMin -= 20 - Math.floor(available[buy]/250); let barterRate = getBarterRate(barterPrices, buy, "food"); if (barterRate < barterRateMin) continue; let choice = barterRate / (100 + available[buy]); if (choice > bestChoice) { bestChoice = choice; bestToBuy = buy; } } if (bestToBuy !== undefined) { let amount = available.food > 5000 ? 500 : 100; barterers[0].barter(bestToBuy, "food", amount); if (this.Config.debug > 2) API3.warn("Contingency bartering: sold food for " + bestToBuy + " available sell " + available.food + " available buy " + available[bestToBuy] + " barterRate " + getBarterRate(barterPrices, bestToBuy, "food") + " amount " + amount); return true; } return false; }; m.TradeManager.prototype.checkEvents = function(gameState, events) { // check if one market from a traderoute is renamed, change the route accordingly for (let evt of events.EntityRenamed) { let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.hasClass("Market")) continue; for (let trader of this.traders.values()) { let route = trader.getMetadata(PlayerID, "route"); if (!route) continue; if (route.source == evt.entity) route.source = evt.newentity; else if (route.target == evt.entity) route.target = evt.newentity; else continue; trader.setMetadata(PlayerID, "route", route); } } // if one market (or market-foundation) is destroyed, we should look for a better route for (let evt of events.Destroy) { if (!evt.entityObj) continue; let ent = evt.entityObj; if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; this.activateProspection(gameState); return true; } // same thing if one market is built for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; this.activateProspection(gameState); return true; } // and same thing for captured markets for (let evt of events.OwnershipChanged) { if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) continue; let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) continue; this.activateProspection(gameState); return true; } // or if diplomacy changed if (events.DiplomacyChanged.length) { this.activateProspection(gameState); return true; } return false; }; m.TradeManager.prototype.activateProspection = function(gameState) { this.routeProspection = true; gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); }; /** * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute * If an index is given, it returns the best route with this index or the best land route if index is a land index */ m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) { let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon { this.tradeRoute = undefined; this.potentialTradeRoute = undefined; return false; } let onlyOurs = !market2.hasEntities(); if (onlyOurs) market2 = market1; let candidate = { "gain": 0 }; let potential = { "gain": 0 }; let bestIndex = { "gain": 0 }; let bestLand = { "gain": 0 }; let mapSize = gameState.sharedScript.mapSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let m1 of market1.values()) { if (!m1.position()) continue; let access1 = m.getLandAccess(gameState, m1); let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined; for (let m2 of market2.values()) { if (onlyOurs && m1.id() >= m2.id()) continue; if (!m2.position()) continue; let access2 = m.getLandAccess(gameState, m2); let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined; let land = access1 == access2 ? access1 : undefined; let sea = sea1 && sea1 == sea2 ? sea1 : undefined; if (!land && !sea) continue; if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) continue; let gainMultiplier; if (land && traderTemplatesGains.landGainMultiplier) gainMultiplier = traderTemplatesGains.landGainMultiplier; else if (sea && traderTemplatesGains.navalGainMultiplier) gainMultiplier = traderTemplatesGains.navalGainMultiplier; else continue; let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize)); if (gain < this.minimalGain) continue; if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) { if (accessIndex) { if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex) { if (gain < bestIndex.gain) continue; bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex) { if (gain < bestIndex.gain) continue; bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } else if (gameState.ai.accessibility.regionType[accessIndex] == "land") { if (gain < bestLand.gain) continue; bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } } if (gain < candidate.gain) continue; candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } if (gain < potential.gain) continue; potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } } if (potential.gain < 1) this.potentialTradeRoute = undefined; else this.potentialTradeRoute = potential; if (candidate.gain < 1) { if (this.Config.debug > 2) API3.warn("no better trade route possible"); this.tradeRoute = undefined; return false; } if (this.Config.debug > 1 && this.tradeRoute) { if (candidate.gain > this.tradeRoute.gain) API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain); } else if (this.Config.debug > 1) API3.warn("one trade route set with gain " + candidate.gain); this.tradeRoute = candidate; if (this.Config.chat) { let owner = this.tradeRoute.source.owner(); if (owner == PlayerID) owner = this.tradeRoute.target.owner(); if (owner != PlayerID && !this.warnedAllies[owner]) { // Warn an ally that we have a trade route with him m.chatNewTradeRoute(gameState, owner); this.warnedAllies[owner] = true; } } if (accessIndex) { if (bestIndex.gain > 0) return bestIndex; else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0) return bestLand; return false; } return true; }; /** Called when a market was built or destroyed, and checks if trader orders should be changed */ m.TradeManager.prototype.checkTrader = function(gameState, ent) { let presentRoute = ent.getMetadata(PlayerID, "route"); if (!presentRoute) return; if (!ent.position()) { // This trader is garrisoned, we will decide later (when ungarrisoning) what to do ent.setMetadata(PlayerID, "route", undefined); return; } let access = ent.hasClass("Ship") ? m.getSeaAccess(gameState, ent) : m.getLandAccess(gameState, ent); let possibleRoute = this.checkRoutes(gameState, access); // Warning: presentRoute is from metadata, so contains entity ids if (!possibleRoute || possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target || possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target) { // Trader will be assigned in updateTrader ent.setMetadata(PlayerID, "route", undefined); if (!possibleRoute && !ent.hasClass("Ship")) { let closestBase = m.getBestBase(gameState, ent, true); if (closestBase.accessIndex == access) { let closestBasePos = closestBase.anchor.position(); ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); return; } } ent.stopMoving(); } }; m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) { if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market")) return; if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) return; if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() && !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities()) return; let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market")); if (!template) return; this.checkRoutes(gameState); let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain { // no position found if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}_market")); else this.routeProspection = false; return; } this.routeProspection = false; if (!this.isNewMarketWorth(marketPos[3])) return; // position found, but not enough gain compared to our present route if (this.Config.debug > 1) { if (this.potentialTradeRoute) API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); else API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + marketPos[3]); } if (!this.tradeRoute) gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); if (!this.tradeRoute) plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) { if (expectedGain < this.minimalGain) return false; if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && expectedGain < this.potentialTradeRoute.gain + 20) return false; return true; }; m.TradeManager.prototype.update = function(gameState, events, queues) { if (gameState.ai.HQ.canBarter) this.performBarter(gameState); if (this.Config.difficulty <= 1) return; if (this.checkEvents(gameState, events)) // true if one market was built or destroyed { this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); this.checkRoutes(gameState); } if (this.tradeRoute) { this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); if (gameState.ai.playedTurn % 5 == 0) this.trainMoreTraders(gameState, queues); if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2) gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); if (gameState.ai.playedTurn % 60 == 0) this.setTradingGoods(gameState); } if (this.routeProspection) this.prospectForNewMarket(gameState, queues); }; m.TradeManager.prototype.routeEntToId = function(route) { if (!route) return undefined; let ret = {}; for (let key in route) { if (key == "source" || key == "target") { if (!route[key]) return undefined; ret[key] = route[key].id(); } else ret[key] = route[key]; } return ret; }; m.TradeManager.prototype.routeIdToEnt = function(gameState, route) { if (!route) return undefined; let ret = {}; for (let key in route) { if (key == "source" || key == "target") { ret[key] = gameState.getEntityById(route[key]); if (!ret[key]) return undefined; } else ret[key] = route[key]; } return ret; }; m.TradeManager.prototype.Serialize = function() { return { "tradeRoute": this.routeEntToId(this.tradeRoute), "potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute), "routeProspection": this.routeProspection, "targetNumTraders": this.targetNumTraders, "warnedAllies": this.warnedAllies }; }; m.TradeManager.prototype.Deserialize = function(gameState, data) { for (let key in data) { if (key == "tradeRoute" || key == "potentialTradeRoute") this[key] = this.routeIdToEnt(gameState, data[key]); else this[key] = data[key]; } }; return m; }(PETRA);