From 09671b49a2e528632070632fb9e5ca3b33a216af Mon Sep 17 00:00:00 2001 From: phosit Date: Tue, 14 Jul 2026 12:11:02 +0200 Subject: [PATCH] Use JS classes for QueuePlan Classes derifed from `QueuePlan` used manual inherintance. Refs: #6285 --- .../public/simulation/ai/petra/queueplan.js | 108 +- .../simulation/ai/petra/queueplanBuilding.js | 1702 ++++++++--------- .../simulation/ai/petra/queueplanResearch.js | 192 +- .../simulation/ai/petra/queueplanTraining.js | 269 ++- 4 files changed, 1130 insertions(+), 1141 deletions(-) diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplan.js b/binaries/data/mods/public/simulation/ai/petra/queueplan.js index 53134a6b45..233424ec11 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplan.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplan.js @@ -5,64 +5,62 @@ import { aiWarn } from "simulation/ai/common-api/utils.js"; * Common functions and variables to all queue plans. */ -export function QueuePlan(gameState, type, metadata) +export class QueuePlan { - this.type = gameState.applyCiv(type); - this.metadata = metadata; - - this.template = gameState.getTemplate(this.type); - if (!this.template) + constructor(gameState, type, metadata) + { + this.type = gameState.applyCiv(type); + this.metadata = metadata; + + this.template = gameState.getTemplate(this.type); + if (!this.template) + throw new Error("Tried to add the inexisting template " + this.type + " to Petra."); + this.ID = gameState.ai.uniqueIDs.plans++; + this.cost = new ResourcesManager(this.template.cost()); + this.number = 1; + this.category = ""; + } + + /** Check the content of this queue */ + isInvalid(gameState) { - aiWarn("Tried to add the inexisting template " + this.type + " to Petra."); return false; } - this.ID = gameState.ai.uniqueIDs.plans++; - this.cost = new ResourcesManager(this.template.cost()); - this.number = 1; - this.category = ""; - return true; + /** if true, the queue manager will begin increasing this plan's account. */ + isGo(gameState) + { + return true; + } + + /** can we start this plan immediately? */ + canStart(gameState) + { + return false; + } + + /** process the plan. */ + start(gameState) + { + // should call onStart. + } + + getCost() + { + const costs = new ResourcesManager(); + costs.add(this.cost); + if (this.number !== 1) + costs.multiply(this.number); + return costs; + } + + /** + * On Event functions. + * Can be used to do some specific stuffs + * Need to be updated to actually do something if you want them to. + * this is called by "Start" if it succeeds. + */ + onStart(gameState) + { + } } - -/** Check the content of this queue */ -QueuePlan.prototype.isInvalid = function(gameState) -{ - return false; -}; - -/** if true, the queue manager will begin increasing this plan's account. */ -QueuePlan.prototype.isGo = function(gameState) -{ - return true; -}; - -/** can we start this plan immediately? */ -QueuePlan.prototype.canStart = function(gameState) -{ - return false; -}; - -/** process the plan. */ -QueuePlan.prototype.start = function(gameState) -{ - // should call onStart. -}; - -QueuePlan.prototype.getCost = function() -{ - const costs = new ResourcesManager(); - costs.add(this.cost); - if (this.number !== 1) - costs.multiply(this.number); - return costs; -}; - -/** - * On Event functions. - * Can be used to do some specific stuffs - * Need to be updated to actually do something if you want them to. - * this is called by "Start" if it succeeds. - */ -QueuePlan.prototype.onStart = function(gameState) -{ -}; diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js b/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js index f49812779c..faaa03c677 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js @@ -12,947 +12,945 @@ import { QueuePlan } from "simulation/ai/petra/queueplan.js"; * We'll try to fing a good position if non has been provided */ -export function ConstructionPlan(gameState, type, metadata, position) +export class ConstructionPlan extends QueuePlan { - if (!QueuePlan.call(this, gameState, type, metadata)) - return false; - - this.position = position ? position : 0; - - this.category = "building"; - - return true; -} - -ConstructionPlan.prototype = Object.create(QueuePlan.prototype); - -ConstructionPlan.prototype.canStart = function(gameState) -{ - if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn - return false; - - if (!this.isGo(gameState)) - return false; - - if (!this.template.available(gameState)) - return false; - - return gameState.ai.HQ.buildManager.hasBuilder(this.type); -}; - -ConstructionPlan.prototype.start = function(gameState) -{ - Engine.ProfileStart("Building construction start"); - - // We don't care which builder we assign, since they won't actually do - // the building themselves - all we care about is that there is at least - // one unit that can start the foundation (should always be the case here). - const builder = gameState.findBuilder(this.type); - if (!builder) + constructor(gameState, type, metadata, position) { - aiWarn("petra error: builder not found when starting construction."); - Engine.ProfileStop(); - return; + super(gameState, type, metadata); + + this.position = position ? position : 0; + + this.category = "building"; } - const pos = this.findGoodPosition(gameState); - if (!pos) + canStart(gameState) { - gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); - Engine.ProfileStop(); - return; + if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn + return false; + + if (!this.isGo(gameState)) + return false; + + if (!this.template.available(gameState)) + return false; + + return gameState.ai.HQ.buildManager.hasBuilder(this.type); } - if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") || - gameState.getOwnEntitiesByClass("Market", true).hasEntities())) + start(gameState) { - // Check if this Market is still worth building (others may have been built making it useless). - const tradeManager = gameState.ai.HQ.tradeManager; - tradeManager.checkRoutes(gameState); - if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) + Engine.ProfileStart("Building construction start"); + + // We don't care which builder we assign, since they won't actually do + // the building themselves - all we care about is that there is at least + // one unit that can start the foundation (should always be the case here). + const builder = gameState.findBuilder(this.type); + if (!builder) { + aiWarn("petra error: builder not found when starting construction."); Engine.ProfileStop(); return; } - } - gameState.ai.HQ.turnCache.buildingBuilt = true; - if (this.metadata === undefined) - this.metadata = { "base": pos.base }; - else if (this.metadata.base === undefined) - this.metadata.base = pos.base; - - if (pos.access) - this.metadata.access = pos.access; // needed for Docks whose position is on water - else - this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); - - if (this.template.buildPlacementType() == "shore") - { - // adjust a bit the position if needed - const cosa = Math.cos(pos.angle); - const sina = Math.sin(pos.angle); - const shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) + const pos = this.findGoodPosition(gameState); + if (!pos) { - builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); + gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); + Engine.ProfileStop(); + return; } - } - else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) - builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); - else // try with the lowest, move towards us unless we're same - { - for (let step = 0; step <= 1; step += 0.2) - builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, - pos.angle, this.metadata); - } - this.onStart(gameState); - Engine.ProfileStop(); - if (this.metadata && this.metadata.proximity) - gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); -}; - -ConstructionPlan.prototype.findGoodPosition = function(gameState) -{ - let template = this.template; - - if (template.buildPlacementType() == "shore") - return this.findDockPosition(gameState); - - const HQ = gameState.ai.HQ; - if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) - { - // recompute the best dropsite location in case some conditions have changed - const base = HQ.getBaseByID(this.metadata.base); - const type = this.metadata.type ? this.metadata.type : "wood"; - const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName); - if (newpos && newpos.quality > 0) + if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") || + gameState.getOwnEntitiesByClass("Market", true).hasEntities())) { - const pos = newpos.pos; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; - } - } - - if (!this.position) - { - if (template.hasClass("CivCentre")) - { - let pos; - if (this.metadata && this.metadata.resource) + // Check if this Market is still worth building (others may have been built making it useless). + const tradeManager = gameState.ai.HQ.tradeManager; + tradeManager.checkRoutes(gameState); + if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { - const proximity = this.metadata.proximity ? this.metadata.proximity : undefined; - pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); + Engine.ProfileStop(); + return; + } + } + gameState.ai.HQ.turnCache.buildingBuilt = true; + + if (this.metadata === undefined) + this.metadata = { "base": pos.base }; + else if (this.metadata.base === undefined) + this.metadata.base = pos.base; + + if (pos.access) + this.metadata.access = pos.access; // needed for Docks whose position is on water + else + this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); + + if (this.template.buildPlacementType() == "shore") + { + // adjust a bit the position if needed + const cosa = Math.cos(pos.angle); + const sina = Math.sin(pos.angle); + const shiftMax = gameState.ai.HQ.territoryMap.cellSize; + for (let shift = 0; shift <= shiftMax; shift += 2) + { + builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); + if (shift > 0) + builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); + } + } + else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) + builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); + else // try with the lowest, move towards us unless we're same + { + for (let step = 0; step <= 1; step += 0.2) + builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, + pos.angle, this.metadata); + } + this.onStart(gameState); + Engine.ProfileStop(); + + if (this.metadata && this.metadata.proximity) + gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); + } + + findGoodPosition(gameState) + { + let template = this.template; + + if (template.buildPlacementType() == "shore") + return this.findDockPosition(gameState); + + const HQ = gameState.ai.HQ; + if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) + { + // recompute the best dropsite location in case some conditions have changed + const base = HQ.getBaseByID(this.metadata.base); + const type = this.metadata.type ? this.metadata.type : "wood"; + const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName); + if (newpos && newpos.quality > 0) + { + const pos = newpos.pos; + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; + } + } + + if (!this.position) + { + if (template.hasClass("CivCentre")) + { + let pos; + if (this.metadata && this.metadata.resource) + { + const proximity = this.metadata.proximity ? this.metadata.proximity : undefined; + pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); + } + else + pos = HQ.findStrategicCCLocation(gameState, template); + + if (pos) + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; + // No possible location, try to build instead a dock in a not-enemy island + const templateName = gameState.applyCiv("structures/{civ}/dock"); + if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) + { + template = gameState.getTemplate(templateName); + if (template && gameState.getResources().canAfford( + new ResourcesManager(template.cost()))) + { + this.buildOverseaDock(gameState, template); + } + } + return false; + } + else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"])) + { + const pos = HQ.findDefensiveLocation(gameState, template); + if (pos) + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; + // if this fortress is our first one, just try the standard placement + if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) + return false; + } + else if (template.hasClass("Market")) // Docks are done before. + { + const pos = HQ.findMarketLocation(gameState, template); + if (pos && pos[2] > 0) + { + if (!this.metadata) + this.metadata = {}; + this.metadata.expectedGain = pos[3]; + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; + } + else if (!pos) + return false; + } + } + + // Compute each tile's closeness to friendly structures: + + const placement = new InfoMap(gameState.sharedScript, "territory"); + const cellSize = placement.cellSize; // size of each tile + + let alreadyHasHouses = false; + + if (this.position) // If a position was specified then place the building as close to it as possible + { + const x = Math.floor(this.position[0] / cellSize); + const z = Math.floor(this.position[1] / cellSize); + placement.addInfluence(x, z, 255); + } + else // No position was specified so try and find a sensible place to build + { + // give a small > 0 level as the result of addInfluence is constrained to be > 0 + // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints + if (this.metadata && this.metadata.base !== undefined) + { + const base = this.metadata.base; + for (let j = 0; j < placement.map.length; ++j) + if (HQ.baseAtIndex(j) == base) + placement.set(j, 45); } else - pos = HQ.findStrategicCCLocation(gameState, template); - - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; - // No possible location, try to build instead a dock in a not-enemy island - const templateName = gameState.applyCiv("structures/{civ}/dock"); - if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) { - template = gameState.getTemplate(templateName); - if (template && gameState.getResources().canAfford( - new ResourcesManager(template.cost()))) + for (let j = 0; j < placement.map.length; ++j) + if (HQ.baseAtIndex(j) != 0) + placement.set(j, 45); + } + + if (!HQ.requireHouses || !template.hasClass("House")) + { + gameState.getOwnStructures().forEach(function(ent) { - this.buildOverseaDock(gameState, template); + const pos = ent.position(); + const x = Math.round(pos[0] / cellSize); + const z = Math.round(pos[1] / cellSize); + + const struct = getBuiltEntity(gameState, ent); + if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) + { + if (template.hasClasses(["Field", "Corral"])) + placement.addInfluence(x, z, 80 / cellSize, 50); + else // If this is not a field add a negative influence because we want to leave this area for fields + placement.addInfluence(x, z, 80 / cellSize, -20); + } + else if (template.hasClass("House")) + { + if (ent.hasClass("House")) + { + placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses + alreadyHasHouses = true; + } + else if (ent.hasClasses(["Gate", "!Wall"])) + placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs + } + else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) && + ent.hasClasses(["Gate", "!Wall"])) + placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber) + else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) + placement.addInfluence(x, z, 120 / cellSize, -50); + else if (template.hasClass("Military")) + placement.addInfluence(x, z, 40 / cellSize, -40); + else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) + placement.addInfluence(x, z, 60 / cellSize, 40); + }); + } + if (template.hasClass("Farmstead")) + { + for (let j = 0; j < placement.map.length; ++j) + { + let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; + if (HQ.borderMap.map[j] & mapMask.fullBorder) + value /= 2; // we need space around farmstead, so disfavor map border + placement.set(j, value); } } - return false; } - else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"])) - { - const pos = HQ.findDefensiveLocation(gameState, template); - if (pos) - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - // if this fortress is our first one, just try the standard placement - if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) - return false; - } - else if (template.hasClass("Market")) // Docks are done before. - { - const pos = HQ.findMarketLocation(gameState, template); - if (pos && pos[2] > 0) - { - if (!this.metadata) - this.metadata = {}; - this.metadata.expectedGain = pos[3]; - return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; - } - else if (!pos) - return false; - } - } - // Compute each tile's closeness to friendly structures: - - const placement = new InfoMap(gameState.sharedScript, "territory"); - const cellSize = placement.cellSize; // size of each tile - - let alreadyHasHouses = false; - - if (this.position) // If a position was specified then place the building as close to it as possible - { - const x = Math.floor(this.position[0] / cellSize); - const z = Math.floor(this.position[1] / cellSize); - placement.addInfluence(x, z, 255); - } - else // No position was specified so try and find a sensible place to build - { - // give a small > 0 level as the result of addInfluence is constrained to be > 0 - // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints + // Requires to be inside our territory, and inside our base territory if required + // and if our first market, put it on border if possible to maximize distance with next Market. + const favorBorder = template.hasClass("Market"); + const disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); + const favoredBase = this.metadata && (this.metadata.favoredBase || + (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); if (this.metadata && this.metadata.base !== undefined) { const base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) - if (HQ.baseAtIndex(j) == base) - placement.set(j, 45); + { + if (HQ.baseAtIndex(j) != base) + placement.map[j] = 0; + else if (placement.map[j] > 0) + { + if (favorBorder && HQ.borderMap.map[j] & mapMask.border) + placement.set(j, placement.map[j] + 50); + else if (disfavorBorder && !(HQ.borderMap.map[j] & mapMask.fullBorder)) + placement.set(j, placement.map[j] + 10); + + const x = (j % placement.width + 0.5) * cellSize; + const z = (Math.floor(j / placement.width) + 0.5) * cellSize; + if (HQ.isNearInvadingArmy([x, z])) + placement.map[j] = 0; + } + } } else { for (let j = 0; j < placement.map.length; ++j) - if (HQ.baseAtIndex(j) != 0) - placement.set(j, 45); - } - - if (!HQ.requireHouses || !template.hasClass("House")) - { - gameState.getOwnStructures().forEach(function(ent) { - const pos = ent.position(); - const x = Math.round(pos[0] / cellSize); - const z = Math.round(pos[1] / cellSize); - - const struct = getBuiltEntity(gameState, ent); - if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) - { - if (template.hasClasses(["Field", "Corral"])) - placement.addInfluence(x, z, 80 / cellSize, 50); - else // If this is not a field add a negative influence because we want to leave this area for fields - placement.addInfluence(x, z, 80 / cellSize, -20); - } - else if (template.hasClass("House")) - { - if (ent.hasClass("House")) - { - placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses - alreadyHasHouses = true; - } - else if (ent.hasClasses(["Gate", "!Wall"])) - placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs - } - else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) && - ent.hasClasses(["Gate", "!Wall"])) - placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber) - else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) - placement.addInfluence(x, z, 120 / cellSize, -50); - else if (template.hasClass("Military")) - placement.addInfluence(x, z, 40 / cellSize, -40); - else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) - placement.addInfluence(x, z, 60 / cellSize, 40); - }); - } - if (template.hasClass("Farmstead")) - { - for (let j = 0; j < placement.map.length; ++j) - { - let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; - if (HQ.borderMap.map[j] & mapMask.fullBorder) - value /= 2; // we need space around farmstead, so disfavor map border - placement.set(j, value); - } - } - } - - // Requires to be inside our territory, and inside our base territory if required - // and if our first market, put it on border if possible to maximize distance with next Market. - const favorBorder = template.hasClass("Market"); - const disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); - const favoredBase = this.metadata && (this.metadata.favoredBase || - (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); - if (this.metadata && this.metadata.base !== undefined) - { - const base = this.metadata.base; - for (let j = 0; j < placement.map.length; ++j) - { - if (HQ.baseAtIndex(j) != base) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & mapMask.border) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & mapMask.fullBorder)) - placement.set(j, placement.map[j] + 10); - - const x = (j % placement.width + 0.5) * cellSize; - const z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) + if (HQ.baseAtIndex(j) == 0) placement.map[j] = 0; + else if (placement.map[j] > 0) + { + if (favorBorder && HQ.borderMap.map[j] & mapMask.border) + placement.set(j, placement.map[j] + 50); + else if (disfavorBorder && !(HQ.borderMap.map[j] & mapMask.fullBorder)) + placement.set(j, placement.map[j] + 10); + + const x = (j % placement.width + 0.5) * cellSize; + const z = (Math.floor(j / placement.width) + 0.5) * cellSize; + if (HQ.isNearInvadingArmy([x, z])) + placement.map[j] = 0; + else if (favoredBase && HQ.baseAtIndex(j) == favoredBase) + placement.set(j, placement.map[j] + 100); + } } } - } - else - { - for (let j = 0; j < placement.map.length; ++j) + + // Find the best non-obstructed: + // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, + // this allows room for units to walk between buildings. + // note: not for houses and dropsites who ought to be closer to either each other or a resource. + // also not for fields who can be stacked quite a bit + + const obstructions = createObstructionMap(gameState, 0, template); + // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); + + let radius; + if (template.hasClasses(["Fortress", "Arsenal"]) || + this.type == gameState.applyCiv("structures/{civ}/elephant_stable")) + radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); + else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"])) + radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); + else + radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + + let bestTile; + if (template.hasClass("House") && !alreadyHasHouses) { - if (HQ.baseAtIndex(j) == 0) - placement.map[j] = 0; - else if (placement.map[j] > 0) - { - if (favorBorder && HQ.borderMap.map[j] & mapMask.border) - placement.set(j, placement.map[j] + 50); - else if (disfavorBorder && !(HQ.borderMap.map[j] & mapMask.fullBorder)) - placement.set(j, placement.map[j] + 10); - - const x = (j % placement.width + 0.5) * cellSize; - const z = (Math.floor(j / placement.width) + 0.5) * cellSize; - if (HQ.isNearInvadingArmy([x, z])) - placement.map[j] = 0; - else if (favoredBase && HQ.baseAtIndex(j) == favoredBase) - placement.set(j, placement.map[j] + 100); - } + // try to get some space to place several houses first + bestTile = placement.findBestTile(3*radius, obstructions); + if (!bestTile.val) + bestTile = undefined; } - } - // Find the best non-obstructed: - // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, - // this allows room for units to walk between buildings. - // note: not for houses and dropsites who ought to be closer to either each other or a resource. - // also not for fields who can be stacked quite a bit + if (!bestTile) + bestTile = placement.findBestTile(radius, obstructions); - const obstructions = createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let radius; - if (template.hasClasses(["Fortress", "Arsenal"]) || - this.type == gameState.applyCiv("structures/{civ}/elephant_stable")) - radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); - else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"])) - radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); - else - radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - - let bestTile; - if (template.hasClass("House") && !alreadyHasHouses) - { - // try to get some space to place several houses first - bestTile = placement.findBestTile(3*radius, obstructions); if (!bestTile.val) - bestTile = undefined; + return false; + + const bestIdx = bestTile.idx; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + + const territorypos = placement.gamePosToMapPos([x, z]); + const territoryIndex = territorypos[0] + territorypos[1]*placement.width; + // default angle = 3*Math.PI/4; + return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) }; } - if (!bestTile) - bestTile = placement.findBestTile(radius, obstructions); - - if (!bestTile.val) - return false; - - const bestIdx = bestTile.idx; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - const territorypos = placement.gamePosToMapPos([x, z]); - const territoryIndex = territorypos[0] + territorypos[1]*placement.width; - // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) }; -}; - -/** - * Placement of buildings with Dock build category - * metadata.proximity is defined when first dock without any territory - * => we try to minimize distance from our current point - * metadata.oversea is defined for dock in oversea islands - * => we try to maximize distance to our current docks (for trade) - * otherwise standard dock on an island where we already have a cc - * => we try not to be too far from our territory - * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. - */ -ConstructionPlan.prototype.findDockPosition = function(gameState) -{ - const template = this.template; - const territoryMap = gameState.ai.HQ.territoryMap; - - const obstructions = createObstructionMap(gameState, 0, template); - // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - - let bestIdx; - let bestJdx; - let bestAngle; - let bestLand; - let bestWater; - let bestVal = -1; - const navalPassMap = gameState.ai.accessibility.navalPassMap; - - const width = gameState.ai.HQ.territoryMap.width; - const cellSize = gameState.ai.HQ.territoryMap.cellSize; - - const nbShips = gameState.ai.HQ.navalManager.transportShips.length; - let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; - const wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; - const proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; - const oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; - if (nbShips == 0 && proxyAccess && proxyAccess > 1) + /** + * Placement of buildings with Dock build category + * metadata.proximity is defined when first dock without any territory + * => we try to minimize distance from our current point + * metadata.oversea is defined for dock in oversea islands + * => we try to maximize distance to our current docks (for trade) + * otherwise standard dock on an island where we already have a cc + * => we try not to be too far from our territory + * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. + */ + findDockPosition(gameState) { - wantedLand = {}; - wantedLand[proxyAccess] = true; - } - const dropsiteTypes = template.resourceDropsiteTypes(); - const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); + const template = this.template; + const territoryMap = gameState.ai.HQ.territoryMap; - let halfSize = 0; // used for dock angle - let halfDepth = 0; // used by checkPlacement - let halfWidth = 0; // used by checkPlacement - if (template.get("Footprint/Square")) - { - halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; - halfDepth = +template.get("Footprint/Square/@depth") / 2; - halfWidth = +template.get("Footprint/Square/@width") / 2; - } - else if (template.get("Footprint/Circle")) - { - halfSize = +template.get("Footprint/Circle/@radius"); - halfDepth = halfSize; - halfWidth = halfSize; - } + const obstructions = createObstructionMap(gameState, 0, template); + // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); - // res is a measure of the amount of resources around, and maxRes is the max value taken into account - // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement - const maxRes = 10; - const maxWater = 16; - const ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")) : - null; - const docks = oversea ? gameState.getOwnStructures().filter(filters.byClass("Dock")) : null; - // Normalisation factors (only guessed, no attempt to optimize them) - const factor = proxyAccess ? 1 : oversea ? 0.2 : 40; - for (let j = 0; j < territoryMap.length; ++j) - { - if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) - continue; - let score = 0; - if (!proxyAccess && !oversea) + let bestIdx; + let bestJdx; + let bestAngle; + let bestLand; + let bestWater; + let bestVal = -1; + const navalPassMap = gameState.ai.accessibility.navalPassMap; + + const width = gameState.ai.HQ.territoryMap.width; + const cellSize = gameState.ai.HQ.territoryMap.cellSize; + + const nbShips = gameState.ai.HQ.navalManager.transportShips.length; + let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; + const wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; + const proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; + const oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; + if (nbShips == 0 && proxyAccess && proxyAccess > 1) { - // if not in our (or allied) territory, we do not want it too far to be able to defend it - score = this.getFrontierProximity(gameState, j); - if (score > 4) - continue; - score *= factor; + wantedLand = {}; + wantedLand[proxyAccess] = true; } - const i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) - continue; - if (wantedSea && navalPassMap[i] != wantedSea) - continue; + const dropsiteTypes = template.resourceDropsiteTypes(); + const radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); - const res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; - const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - - // If proximity is given, we look for the nearest point - if (proxyAccess) - score = VectorDistance(this.metadata.proximity, pos); - - // Bonus for resources - score += 20 * (maxRes - res); - - if (oversea) + let halfSize = 0; // used for dock angle + let halfDepth = 0; // used by checkPlacement + let halfWidth = 0; // used by checkPlacement + if (template.get("Footprint/Square")) { - // Not much farther to one of our cc than to enemy ones - let enemyDist; - let ownDist; - for (const cc of ccEnts.values()) + halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; + halfDepth = +template.get("Footprint/Square/@depth") / 2; + halfWidth = +template.get("Footprint/Square/@width") / 2; + } + else if (template.get("Footprint/Circle")) + { + halfSize = +template.get("Footprint/Circle/@radius"); + halfDepth = halfSize; + halfWidth = halfSize; + } + + // res is a measure of the amount of resources around, and maxRes is the max value taken into account + // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement + const maxRes = 10; + const maxWater = 16; + const ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")) : + null; + const docks = oversea ? gameState.getOwnStructures().filter(filters.byClass("Dock")) : null; + // Normalisation factors (only guessed, no attempt to optimize them) + const factor = proxyAccess ? 1 : oversea ? 0.2 : 40; + for (let j = 0; j < territoryMap.length; ++j) + { + if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) + continue; + let score = 0; + if (!proxyAccess && !oversea) { - const owner = cc.owner(); - if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) + // if not in our (or allied) territory, we do not want it too far to be able to defend it + score = this.getFrontierProximity(gameState, j); + if (score > 4) continue; - const dist = SquareVectorDistance(pos, cc.position()); - if (owner == PlayerID && (!ownDist || dist < ownDist)) - ownDist = dist; - if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) - enemyDist = dist; + score *= factor; } - if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) + const i = territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + if (wantedSea && navalPassMap[i] != wantedSea) continue; - // And maximize distance for trade. - let dockDist = 0; + const res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + + // If proximity is given, we look for the nearest point + if (proxyAccess) + score = VectorDistance(this.metadata.proximity, pos); + + // Bonus for resources + score += 20 * (maxRes - res); + + if (oversea) + { + // Not much farther to one of our cc than to enemy ones + let enemyDist; + let ownDist; + for (const cc of ccEnts.values()) + { + const owner = cc.owner(); + if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) + continue; + const dist = SquareVectorDistance(pos, cc.position()); + if (owner == PlayerID && (!ownDist || dist < ownDist)) + ownDist = dist; + if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) + enemyDist = dist; + } + if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) + continue; + + // And maximize distance for trade. + let dockDist = 0; + for (const dock of docks.values()) + { + if (getSeaAccess(gameState, dock) != navalPassMap[i]) + continue; + const dist = SquareVectorDistance(pos, dock.position()); + if (dist > dockDist) + dockDist = dist; + } + if (dockDist > 0) + { + dockDist = Math.sqrt(dockDist); + if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away + continue; + score += factor * (width * cellSize - dockDist); + } + } + + // Add a penalty if on the map border as ship movement will be difficult + if (gameState.ai.HQ.borderMap.map[j] & mapMask.fullBorder) + score += 20; + + // Do a pre-selection, supposing we will have the best possible water + if (bestIdx !== undefined && score > bestVal + 5 * maxWater) + continue; + + const x = (i % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + const angle = this.getDockAngle(gameState, x, z, halfSize); + if (angle == false) + continue; + const ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); + if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) + continue; + // Final selection now that the checkDockPlacement water is known + if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) + continue; + if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) + continue; + if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) + continue; + + bestVal = score + maxWater - ret.water; + bestIdx = i; + bestJdx = j; + bestAngle = angle; + bestLand = ret.land; + bestWater = ret.water; + } + if (bestVal < 0) + return false; + + // if no good place with enough water around and still in first phase, wait for expansion at the next phase + if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) + return false; + + const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + + // Assign this dock to a base + let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx); + if (!baseIndex) + baseIndex = -2; // We'll do an anchorless base around it + + return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; + } + + /** + * Find a good island to build a dock. + */ + buildOverseaDock(gameState, template) + { + const docks = gameState.getOwnStructures().filter(filters.byClass("Dock")); + if (!docks.hasEntities()) + return; + + const passabilityMap = gameState.getPassabilityMap(); + const cellArea = passabilityMap.cellSize * passabilityMap.cellSize; + const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); + + const land = {}; + let found; + for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) + { + if (gameState.ai.accessibility.regionType[i] != "land" || + cellArea * gameState.ai.accessibility.regionSize[i] < 3600) + continue; + let keep = true; for (const dock of docks.values()) { - if (getSeaAccess(gameState, dock) != navalPassMap[i]) + if (getLandAccess(gameState, dock) != i) continue; - const dist = SquareVectorDistance(pos, dock.position()); - if (dist > dockDist) - dockDist = dist; - } - if (dockDist > 0) - { - dockDist = Math.sqrt(dockDist); - if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away - continue; - score += factor * (width * cellSize - dockDist); - } - } - - // Add a penalty if on the map border as ship movement will be difficult - if (gameState.ai.HQ.borderMap.map[j] & mapMask.fullBorder) - score += 20; - - // Do a pre-selection, supposing we will have the best possible water - if (bestIdx !== undefined && score > bestVal + 5 * maxWater) - continue; - - const x = (i % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; - const angle = this.getDockAngle(gameState, x, z, halfSize); - if (angle == false) - continue; - const ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); - if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) - continue; - // Final selection now that the checkDockPlacement water is known - if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) - continue; - if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) - continue; - if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) - continue; - - bestVal = score + maxWater - ret.water; - bestIdx = i; - bestJdx = j; - bestAngle = angle; - bestLand = ret.land; - bestWater = ret.water; - } - if (bestVal < 0) - return false; - - // if no good place with enough water around and still in first phase, wait for expansion at the next phase - if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) - return false; - - const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; - const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - - // Assign this dock to a base - let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx); - if (!baseIndex) - baseIndex = -2; // We'll do an anchorless base around it - - return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; -}; - -/** - * Find a good island to build a dock. - */ -ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) -{ - const docks = gameState.getOwnStructures().filter(filters.byClass("Dock")); - if (!docks.hasEntities()) - return; - - const passabilityMap = gameState.getPassabilityMap(); - const cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")); - - const land = {}; - let found; - for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (gameState.ai.accessibility.regionType[i] != "land" || - cellArea * gameState.ai.accessibility.regionSize[i] < 3600) - continue; - let keep = true; - for (const dock of docks.values()) - { - if (getLandAccess(gameState, dock) != i) - continue; - keep = false; - break; - } - if (!keep) - continue; - let sea; - for (const cc of ccEnts.values()) - { - const ccAccess = getLandAccess(gameState, cc); - if (ccAccess != i) - { - if (cc.owner() == PlayerID && !sea) - sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); - continue; - } - // Docks on island where we have a cc are already done elsewhere - if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) - { keep = false; break; } + if (!keep) + continue; + let sea; + for (const cc of ccEnts.values()) + { + const ccAccess = getLandAccess(gameState, cc); + if (ccAccess != i) + { + if (cc.owner() == PlayerID && !sea) + sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); + continue; + } + // Docks on island where we have a cc are already done elsewhere + if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) + { + keep = false; + break; + } + } + if (!keep || !sea) + continue; + land[i] = true; + found = true; } - if (!keep || !sea) - continue; - land[i] = true; - found = true; - } - if (!found) - return; - if (!gameState.ai.HQ.navalMap) - aiWarn("petra.findOverseaLand on a non-naval map??? we should never go there "); + if (!found) + return; + if (!gameState.ai.HQ.navalMap) + aiWarn("petra.findOverseaLand on a non-naval map??? we should never go there "); - const oldTemplate = this.template; - const oldMetadata = this.metadata; - this.template = template; - this.metadata = { "land": land, "oversea": true }; - const pos = this.findDockPosition(gameState); - if (pos) - { - const type = template.templateName(); - const builder = gameState.findBuilder(type); - this.metadata.base = pos.base; - // Adjust a bit the position if needed - const cosa = Math.cos(pos.angle); - const sina = Math.sin(pos.angle); - const shiftMax = gameState.ai.HQ.territoryMap.cellSize; - for (let shift = 0; shift <= shiftMax; shift += 2) + const oldTemplate = this.template; + const oldMetadata = this.metadata; + this.template = template; + this.metadata = { "land": land, "oversea": true }; + const pos = this.findDockPosition(gameState); + if (pos) { - builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); - if (shift > 0) - builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); + const type = template.templateName(); + const builder = gameState.findBuilder(type); + this.metadata.base = pos.base; + // Adjust a bit the position if needed + const cosa = Math.cos(pos.angle); + const sina = Math.sin(pos.angle); + const shiftMax = gameState.ai.HQ.territoryMap.cellSize; + for (let shift = 0; shift <= shiftMax; shift += 2) + { + builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); + if (shift > 0) + builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); + } } + this.template = oldTemplate; + this.metadata = oldMetadata; } - this.template = oldTemplate; - this.metadata = oldMetadata; -}; -/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ -ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) -{ - let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); - const k = pos[0] + pos[1]*gameState.ai.accessibility.width; - const seaRef = gameState.ai.accessibility.navalPassMap[k]; - if (seaRef < 2) + /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ + getDockAngle(gameState, x, z, size) + { + let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); + const k = pos[0] + pos[1]*gameState.ai.accessibility.width; + const seaRef = gameState.ai.accessibility.navalPassMap[k]; + if (seaRef < 2) + return false; + const numPoints = 16; + for (let dist = 0; dist < 4; ++dist) + { + const waterPoints = []; + for (let i = 0; i < numPoints; ++i) + { + const angle = 2 * Math.PI * i / numPoints; + pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; + pos = gameState.ai.accessibility.gamePosToMapPos(pos); + if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || + pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + continue; + const j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.navalPassMap[j] == seaRef) + waterPoints.push(i); + } + const length = waterPoints.length; + if (!length) + continue; + const consec = []; + for (let i = 0; i < length; ++i) + { + let count = 0; + for (let j = 0; j < length-1; ++j) + { + if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) + ++count; + else + break; + } + consec[i] = count; + } + let start = 0; + let count = 0; + for (const c in consec) + { + if (consec[c] > count) + { + start = c; + count = consec[c]; + } + } + + // If we've found a shoreline, stop searching + if (count != numPoints-1) + return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; + } return false; - const numPoints = 16; - for (let dist = 0; dist < 4; ++dist) + } + + /** + * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js + * to determine the special dock requirements + * returns {"land": land index for this dock, "water": amount of water around this spot} + */ + checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle) { - const waterPoints = []; - for (let i = 0; i < numPoints; ++i) + const sz = halfDepth * Math.sin(angle); + const cz = halfDepth * Math.cos(angle); + // center back position + let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); + let j = pos[0] + pos[1]*gameState.ai.accessibility.width; + const land = gameState.ai.accessibility.landPassMap[j]; + if (land < 2) + return null; + // center front position + pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) + return null; + // additional constraints compared to BuildRestriction.js to assure we have enough place to build + const sw = halfWidth * Math.cos(angle) * 3 / 4; + const cw = halfWidth * Math.sin(angle) * 3 / 4; + pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] != land) + return null; + pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] != land) + return null; + let water = 0; + const sp = 15 * Math.sin(angle); + const cp = 15 * Math.cos(angle); + for (let i = 1; i < 5; ++i) { - const angle = 2 * Math.PI * i / numPoints; - pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; - pos = gameState.ai.accessibility.gamePosToMapPos(pos); + pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + break; + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) + break; + pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); + if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || + pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + break; + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) + break; + pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); + if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || + pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + break; + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) + break; + water += 4; + } + return { "land": land, "water": water }; + } + + /** + * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension + * if the (object) wantedLand is given, this nearest land should have one of these accessibility + * if wantedSea is given, this tile should be inside this sea + */ + static around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], + [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]]; + + isDockLocation(gameState, j, dimension, wantedLand, wantedSea) + { + const width = gameState.ai.HQ.territoryMap.width; + const cellSize = gameState.ai.HQ.territoryMap.cellSize; + const dimLand = dimension + 1.5 * cellSize; + const dimSea = dimension + 2 * cellSize; + + const accessibility = gameState.ai.accessibility; + const x = (j%width + 0.5) * cellSize; + const z = (Math.floor(j/width) + 0.5) * cellSize; + let pos = accessibility.gamePosToMapPos([x, z]); + let k = pos[0] + pos[1]*accessibility.width; + let landPass = accessibility.landPassMap[k]; + if (landPass > 1 && wantedLand && !wantedLand[landPass] || + landPass < 2 && accessibility.navalPassMap[k] < 2) + return false; + + for (const a of ConstructionPlan.around) + { + pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); + if (pos[0] < 0 || pos[0] >= accessibility.width) continue; - const j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.navalPassMap[j] == seaRef) - waterPoints.push(i); - } - const length = waterPoints.length; - if (!length) - continue; - const consec = []; - for (let i = 0; i < length; ++i) - { - let count = 0; - for (let j = 0; j < length-1; ++j) - { - if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) - ++count; - else - break; - } - consec[i] = count; - } - let start = 0; - let count = 0; - for (const c in consec) - { - if (consec[c] > count) - { - start = c; - count = consec[c]; - } + if (pos[1] < 0 || pos[1] >= accessibility.height) + continue; + k = pos[0] + pos[1]*accessibility.width; + landPass = accessibility.landPassMap[k]; + if (landPass < 2 || wantedLand && !wantedLand[landPass]) + continue; + pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); + if (pos[0] < 0 || pos[0] >= accessibility.width) + continue; + if (pos[1] < 0 || pos[1] >= accessibility.height) + continue; + k = pos[0] + pos[1]*accessibility.width; + if (wantedSea && accessibility.navalPassMap[k] != wantedSea || + !wantedSea && accessibility.navalPassMap[k] < 2) + continue; + return true; } - // If we've found a shoreline, stop searching - if (count != numPoints-1) - return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; - } - return false; -}; - -/** - * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js - * to determine the special dock requirements - * returns {"land": land index for this dock, "water": amount of water around this spot} - */ -ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) -{ - const sz = halfDepth * Math.sin(angle); - const cz = halfDepth * Math.cos(angle); - // center back position - let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); - let j = pos[0] + pos[1]*gameState.ai.accessibility.width; - const land = gameState.ai.accessibility.landPassMap[j]; - if (land < 2) - return null; - // center front position - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - return null; - // additional constraints compared to BuildRestriction.js to assure we have enough place to build - const sw = halfWidth * Math.cos(angle) * 3 / 4; - const cw = halfWidth * Math.sin(angle) * 3 / 4; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] != land) - return null; - let water = 0; - const sp = 15 * Math.sin(angle); - const cp = 15 * Math.cos(angle); - for (let i = 1; i < 5; ++i) - { - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); - if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || - pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) - break; - j = pos[0] + pos[1]*gameState.ai.accessibility.width; - if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) - break; - water += 4; - } - return { "land": land, "water": water }; -}; - -/** - * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension - * if the (object) wantedLand is given, this nearest land should have one of these accessibility - * if wantedSea is given, this tile should be inside this sea - */ -ConstructionPlan.prototype.around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], - [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]]; - -ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) -{ - const width = gameState.ai.HQ.territoryMap.width; - const cellSize = gameState.ai.HQ.territoryMap.cellSize; - const dimLand = dimension + 1.5 * cellSize; - const dimSea = dimension + 2 * cellSize; - - const accessibility = gameState.ai.accessibility; - const x = (j%width + 0.5) * cellSize; - const z = (Math.floor(j/width) + 0.5) * cellSize; - let pos = accessibility.gamePosToMapPos([x, z]); - let k = pos[0] + pos[1]*accessibility.width; - let landPass = accessibility.landPassMap[k]; - if (landPass > 1 && wantedLand && !wantedLand[landPass] || - landPass < 2 && accessibility.navalPassMap[k] < 2) return false; + } - for (const a of this.around) + /** + * return a measure of the proximity to our frontier (including our allies) + * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m + */ + getFrontierProximity(gameState, j) { - pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - landPass = accessibility.landPassMap[k]; - if (landPass < 2 || wantedLand && !wantedLand[landPass]) - continue; - pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); - if (pos[0] < 0 || pos[0] >= accessibility.width) - continue; - if (pos[1] < 0 || pos[1] >= accessibility.height) - continue; - k = pos[0] + pos[1]*accessibility.width; - if (wantedSea && accessibility.navalPassMap[k] != wantedSea || - !wantedSea && accessibility.navalPassMap[k] < 2) - continue; + const alliedVictory = gameState.getAlliedVictory(); + const territoryMap = gameState.ai.HQ.territoryMap; + let territoryOwner = territoryMap.getOwnerIndex(j); + if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) + return 0; + + const borderMap = gameState.ai.HQ.borderMap; + const width = territoryMap.width; + const step = Math.round(24 / territoryMap.cellSize); + const ix = j % width; + const iz = Math.floor(j / width); + let best = 5; + for (const a of ConstructionPlan.around) + { + for (let i = 1; i < 5; ++i) + { + const jx = ix + Math.round(i*step*a[0]); + if (jx < 0 || jx >= width) + continue; + const jz = iz + Math.round(i*step*a[1]); + if (jz < 0 || jz >= width) + continue; + if (borderMap.map[jx+width*jz] & mapMask.outside) + continue; + territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); + if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) + { + best = Math.min(best, i); + break; + } + } + if (best == 1) + break; + } + + return best; + } + + /** + * get the sum of the resources (except food) around, inside a given radius + * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood + */ + getResourcesAround(gameState, types, i, radius) + { + const resourceMaps = gameState.sharedScript.resourceMaps; + const w = resourceMaps.wood.width; + const cellSize = resourceMaps.wood.cellSize; + const size = Math.floor(radius / cellSize); + const ix = i % w; + const iy = Math.floor(i / w); + let total = 0; + let nbcell = 0; + for (const k of types) + { + if (k == "food" || !resourceMaps[k]) + continue; + const weigh0 = k == "wood" ? 2 : 1; + for (let dy = 0; dy <= size; ++dy) + { + const dxmax = size - dy; + let ky = iy + dy; + if (ky >= 0 && ky < w) + { + for (let dx = -dxmax; dx <= dxmax; ++dx) + { + const kx = ix + dx; + if (kx < 0 || kx >= w) + continue; + const ddx = dx > 0 ? dx : -dx; + const weight = weigh0 * (dxmax - ddx) / size; + total += weight * resourceMaps[k].map[kx + w * ky]; + nbcell += weight; + } + } + if (dy == 0) + continue; + ky = iy - dy; + if (ky >= 0 && ky < w) + { + for (let dx = -dxmax; dx <= dxmax; ++dx) + { + const kx = ix + dx; + if (kx < 0 || kx >= w) + continue; + const ddx = dx > 0 ? dx : -dx; + const weight = weigh0 * (dxmax - ddx) / size; + total += weight * resourceMaps[k].map[kx + w * ky]; + nbcell += weight; + } + } + } + } + return nbcell ? total / nbcell : 0; + } + + isGo(gameState) + { + if (this.goRequirement && this.goRequirement == "houseNeeded") + { + if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") && + !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment")) + return false; + if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) + return false; + let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); + for (const ent of gameState.getOwnFoundations().values()) + { + const template = gameState.getBuiltTemplate(ent.templateName()); + if (template) + freeSlots += template.getPopulationBonus(); + } + + if (gameState.ai.HQ.saveResources) + return freeSlots <= 10; + if (gameState.getPopulation() > 55) + return freeSlots <= 21; + if (gameState.getPopulation() > 30) + return freeSlots <= 15; + return freeSlots <= 10; + } return true; } - return false; -}; - -/** - * return a measure of the proximity to our frontier (including our allies) - * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m - */ -ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) -{ - const alliedVictory = gameState.getAlliedVictory(); - const territoryMap = gameState.ai.HQ.territoryMap; - let territoryOwner = territoryMap.getOwnerIndex(j); - if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) - return 0; - - const borderMap = gameState.ai.HQ.borderMap; - const width = territoryMap.width; - const step = Math.round(24 / territoryMap.cellSize); - const ix = j % width; - const iz = Math.floor(j / width); - let best = 5; - for (const a of this.around) + onStart(gameState) { - for (let i = 1; i < 5; ++i) - { - const jx = ix + Math.round(i*step*a[0]); - if (jx < 0 || jx >= width) - continue; - const jz = iz + Math.round(i*step*a[1]); - if (jz < 0 || jz >= width) - continue; - if (borderMap.map[jx+width*jz] & mapMask.outside) - continue; - territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); - if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) - { - best = Math.min(best, i); - break; - } - } - if (best == 1) - break; + if (this.queueToReset) + gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); } - return best; -}; - -/** - * get the sum of the resources (except food) around, inside a given radius - * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood - */ -ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) -{ - const resourceMaps = gameState.sharedScript.resourceMaps; - const w = resourceMaps.wood.width; - const cellSize = resourceMaps.wood.cellSize; - const size = Math.floor(radius / cellSize); - const ix = i % w; - const iy = Math.floor(i / w); - let total = 0; - let nbcell = 0; - for (const k of types) + Serialize() { - if (k == "food" || !resourceMaps[k]) - continue; - const weigh0 = k == "wood" ? 2 : 1; - for (let dy = 0; dy <= size; ++dy) - { - const dxmax = size - dy; - let ky = iy + dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - const kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - const ddx = dx > 0 ? dx : -dx; - const weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - if (dy == 0) - continue; - ky = iy - dy; - if (ky >= 0 && ky < w) - { - for (let dx = -dxmax; dx <= dxmax; ++dx) - { - const kx = ix + dx; - if (kx < 0 || kx >= w) - continue; - const ddx = dx > 0 ? dx : -dx; - const weight = weigh0 * (dxmax - ddx) / size; - total += weight * resourceMaps[k].map[kx + w * ky]; - nbcell += weight; - } - } - } + return { + "category": this.category, + "type": this.type, + "ID": this.ID, + "metadata": this.metadata, + "cost": this.cost.Serialize(), + "number": this.number, + "position": this.position, + "goRequirement": this.goRequirement || undefined, + "queueToReset": this.queueToReset || undefined + }; } - return nbcell ? total / nbcell : 0; -}; -ConstructionPlan.prototype.isGo = function(gameState) -{ - if (this.goRequirement && this.goRequirement == "houseNeeded") + Deserialize(gameState, data) { - if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") && - !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment")) - return false; - if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) - return false; - let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); - for (const ent of gameState.getOwnFoundations().values()) - { - const template = gameState.getBuiltTemplate(ent.templateName()); - if (template) - freeSlots += template.getPopulationBonus(); - } + for (const key in data) + this[key] = data[key]; - if (gameState.ai.HQ.saveResources) - return freeSlots <= 10; - if (gameState.getPopulation() > 55) - return freeSlots <= 21; - if (gameState.getPopulation() > 30) - return freeSlots <= 15; - return freeSlots <= 10; + this.cost = new ResourcesManager(); + this.cost.Deserialize(data.cost); } - return true; -}; - -ConstructionPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); -}; - -ConstructionPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "position": this.position, - "goRequirement": this.goRequirement || undefined, - "queueToReset": this.queueToReset || undefined - }; -}; - -ConstructionPlan.prototype.Deserialize = function(gameState, data) -{ - for (const key in data) - this[key] = data[key]; - - this.cost = new ResourcesManager(); - this.cost.Deserialize(data.cost); -}; +} diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js b/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js index 51385dbb6a..f7ba3b4271 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplanResearch.js @@ -1,110 +1,108 @@ import { ResourcesManager } from "simulation/ai/common-api/resources.js"; import { QueuePlan } from "simulation/ai/petra/queueplan.js"; -export function ResearchPlan(gameState, type, rush = false) +export class ResearchPlan extends QueuePlan { - if (!QueuePlan.call(this, gameState, type, {})) - return false; - - if (this.template.researchTime === undefined) - return false; - - // Refine the estimated cost - const researchers = this.getBestResearchers(gameState, true); - if (researchers) - this.cost = new ResourcesManager(this.template.cost(researchers[0])); - - this.category = "technology"; - this.rush = rush; - - return true; -} - -ResearchPlan.prototype = Object.create(QueuePlan.prototype); - -ResearchPlan.prototype.canStart = function(gameState) -{ - this.researchers = this.getBestResearchers(gameState); - if (!this.researchers) - return false; - this.cost = new ResourcesManager(this.template.cost(this.researchers[0])); - return true; -}; - -ResearchPlan.prototype.getBestResearchers = function(gameState, noRequirementCheck = false) -{ - const allResearchers = gameState.findResearchers(this.type, noRequirementCheck); - if (!allResearchers || !allResearchers.hasEntities()) - return undefined; - - // Keep only researchers with smallest cost - let costMin = Math.min(); - let researchers; - for (const ent of allResearchers.values()) + constructor(gameState, type, rush = false) { - const cost = this.template.costSum(ent); - if (cost === costMin) - researchers.push(ent); - else if (cost < costMin) + super(gameState, type, {}); + + if (this.template.researchTime === undefined) + throw new Error(); + + // Refine the estimated cost + const researchers = this.getBestResearchers(gameState, true); + if (researchers) + this.cost = new ResourcesManager(this.template.cost(researchers[0])); + + this.category = "technology"; + this.rush = rush; + } + + canStart(gameState) + { + this.researchers = this.getBestResearchers(gameState); + if (!this.researchers) + return false; + this.cost = new ResourcesManager(this.template.cost(this.researchers[0])); + return true; + } + + getBestResearchers(gameState, noRequirementCheck = false) + { + const allResearchers = gameState.findResearchers(this.type, noRequirementCheck); + if (!allResearchers || !allResearchers.hasEntities()) + return undefined; + + // Keep only researchers with smallest cost + let costMin = Math.min(); + let researchers; + for (const ent of allResearchers.values()) { - costMin = cost; - researchers = [ent]; + const cost = this.template.costSum(ent); + if (cost === costMin) + researchers.push(ent); + else if (cost < costMin) + { + costMin = cost; + researchers = [ent]; + } + } + return researchers; + } + + isInvalid(gameState) + { + return gameState.isResearched(this.type) || gameState.isResearching(this.type); + } + + start(gameState) + { + // Prefer researcher with shortest queues (no need to serialize this.researchers + // as the functions canStart and start are always called on the same turn) + this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); + // Drop anything in the queue if we rush it. + if (this.rush) + this.researchers[0].stopAllProduction(0.45); + this.researchers[0].research(this.type); + this.onStart(gameState); + } + + onStart(gameState) + { + if (this.queueToReset) + gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); + + for (let i = gameState.getNumberOfPhases(); i > 0; --i) + { + if (this.type != gameState.getPhaseName(i)) + continue; + gameState.ai.HQ.phasing = 0; + gameState.ai.HQ.OnPhaseUp(gameState, i); + break; } } - return researchers; -}; -ResearchPlan.prototype.isInvalid = function(gameState) -{ - return gameState.isResearched(this.type) || gameState.isResearching(this.type); -}; - -ResearchPlan.prototype.start = function(gameState) -{ - // Prefer researcher with shortest queues (no need to serialize this.researchers - // as the functions canStart and start are always called on the same turn) - this.researchers.sort((a, b) => a.trainingQueueTime() - b.trainingQueueTime()); - // Drop anything in the queue if we rush it. - if (this.rush) - this.researchers[0].stopAllProduction(0.45); - this.researchers[0].research(this.type); - this.onStart(gameState); -}; - -ResearchPlan.prototype.onStart = function(gameState) -{ - if (this.queueToReset) - gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); - - for (let i = gameState.getNumberOfPhases(); i > 0; --i) + Serialize() { - if (this.type != gameState.getPhaseName(i)) - continue; - gameState.ai.HQ.phasing = 0; - gameState.ai.HQ.OnPhaseUp(gameState, i); - break; + return { + "category": this.category, + "type": this.type, + "ID": this.ID, + "metadata": this.metadata, + "cost": this.cost.Serialize(), + "number": this.number, + "rush": this.rush, + "queueToReset": this.queueToReset || undefined + }; } -}; -ResearchPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "rush": this.rush, - "queueToReset": this.queueToReset || undefined - }; -}; + Deserialize(gameState, data) + { + for (const key in data) + this[key] = data[key]; -ResearchPlan.prototype.Deserialize = function(gameState, data) -{ - for (const key in data) - this[key] = data[key]; - - this.cost = new ResourcesManager(); - this.cost.Deserialize(data.cost); -}; + this.cost = new ResourcesManager(); + this.cost.Deserialize(data.cost); + } +} diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js b/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js index 0c556288e5..663fef3d08 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js @@ -4,162 +4,157 @@ import { aiWarn } from "simulation/ai/common-api/utils.js"; import { QueuePlan } from "simulation/ai/petra/queueplan.js"; import { Worker } from "simulation/ai/petra/worker.js"; -export function TrainingPlan(gameState, type, metadata, number = 1, maxMerge = 5) +export class TrainingPlan extends QueuePlan { - if (!QueuePlan.call(this, gameState, type, metadata)) + constructor(gameState, type, metadata, number = 1, maxMerge = 5) { - aiWarn(" Plan training " + type + " canceled"); - return false; + super(gameState, type, metadata); + + // Refine the estimated cost and add pop cost + const trainers = this.getBestTrainers(gameState); + const trainer = trainers ? trainers[0] : undefined; + this.cost = new ResourcesManager(this.template.cost(trainer), +this.template._template.Cost.Population); + + this.category = "unit"; + this.number = number; + this.maxMerge = maxMerge; } - // Refine the estimated cost and add pop cost - const trainers = this.getBestTrainers(gameState); - const trainer = trainers ? trainers[0] : undefined; - this.cost = new ResourcesManager(this.template.cost(trainer), +this.template._template.Cost.Population); - - this.category = "unit"; - this.number = number; - this.maxMerge = maxMerge; - - return true; -} - -TrainingPlan.prototype = Object.create(QueuePlan.prototype); - -TrainingPlan.prototype.canStart = function(gameState) -{ - this.trainers = this.getBestTrainers(gameState); - if (!this.trainers) - return false; - this.cost = new ResourcesManager(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); - return true; -}; - -TrainingPlan.prototype.getBestTrainers = function(gameState) -{ - if (this.metadata && this.metadata.trainer) + canStart(gameState) { - const trainer = gameState.getEntityById(this.metadata.trainer); - if (trainer) - return [trainer]; + this.trainers = this.getBestTrainers(gameState); + if (!this.trainers) + return false; + this.cost = new ResourcesManager(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); + return true; } - let allTrainers = gameState.findTrainers(this.type); - if (this.metadata && this.metadata.sea) - allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "sea", this.metadata.sea)); - if (this.metadata && this.metadata.base) - allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "base", this.metadata.base)); - if (!allTrainers || !allTrainers.hasEntities()) - return undefined; - - // Keep only trainers with smallest cost - let costMin = Math.min(); - let trainers; - for (const ent of allTrainers.values()) + getBestTrainers(gameState) { - const cost = this.template.costSum(ent); - if (cost === costMin) - trainers.push(ent); - else if (cost < costMin) + if (this.metadata && this.metadata.trainer) { - costMin = cost; - trainers = [ent]; + const trainer = gameState.getEntityById(this.metadata.trainer); + if (trainer) + return [trainer]; } - } - return trainers; -}; -TrainingPlan.prototype.start = function(gameState) -{ - if (this.metadata && this.metadata.trainer) - { - const metadata = {}; - for (const key in this.metadata) - if (key !== "trainer") - metadata[key] = this.metadata[key]; - this.metadata = metadata; - } + let allTrainers = gameState.findTrainers(this.type); + if (this.metadata && this.metadata.sea) + allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "sea", this.metadata.sea)); + if (this.metadata && this.metadata.base) + allTrainers = allTrainers.filter(filters.byMetadata(PlayerID, "base", this.metadata.base)); + if (!allTrainers || !allTrainers.hasEntities()) + return undefined; - if (this.trainers.length > 1) - { - let wantedIndex; - if (this.metadata && this.metadata.index) - wantedIndex = this.metadata.index; - const workerUnit = this.metadata && this.metadata.role && - this.metadata.role === Worker.ROLE_WORKER; - const supportUnit = this.template.hasClass("Support"); - this.trainers.sort(function(a, b) + // Keep only trainers with smallest cost + let costMin = Math.min(); + let trainers; + for (const ent of allTrainers.values()) { - // Prefer training buildings with short queues - let aa = a.trainingQueueTime(); - let bb = b.trainingQueueTime(); - // Give priority to support units in the cc - if (a.hasClass("Civic") && !supportUnit) - aa += 10; - if (b.hasClass("Civic") && !supportUnit) - bb += 10; - // And support units should not be too near to dangerous place - if (supportUnit) + const cost = this.template.costSum(ent); + if (cost === costMin) + trainers.push(ent); + else if (cost < costMin) { - if (gameState.ai.HQ.isNearInvadingArmy(a.position())) - aa += 50; - if (gameState.ai.HQ.isNearInvadingArmy(b.position())) - bb += 50; + costMin = cost; + trainers = [ent]; } - // Give also priority to buildings with the right accessibility - const aBase = a.getMetadata(PlayerID, "base"); - const bBase = b.getMetadata(PlayerID, "base"); - if (wantedIndex) - { - if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) - aa += 30; - if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) - bb += 30; - } - // Then, if workers, small preference for bases with less workers - if (workerUnit && aBase && bBase && aBase != bBase) - { - const apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; - const bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; - if (apop > bpop) - aa++; - else if (bpop > apop) - bb++; - } - return aa - bb; - }); + } + return trainers; } - if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) - this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); - this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata); + start(gameState) + { + if (this.metadata && this.metadata.trainer) + { + const metadata = {}; + for (const key in this.metadata) + if (key !== "trainer") + metadata[key] = this.metadata[key]; + this.metadata = metadata; + } - this.onStart(gameState); -}; + if (this.trainers.length > 1) + { + let wantedIndex; + if (this.metadata && this.metadata.index) + wantedIndex = this.metadata.index; + const workerUnit = this.metadata && this.metadata.role && + this.metadata.role === Worker.ROLE_WORKER; + const supportUnit = this.template.hasClass("Support"); + this.trainers.sort(function(a, b) + { + // Prefer training buildings with short queues + let aa = a.trainingQueueTime(); + let bb = b.trainingQueueTime(); + // Give priority to support units in the cc + if (a.hasClass("Civic") && !supportUnit) + aa += 10; + if (b.hasClass("Civic") && !supportUnit) + bb += 10; + // And support units should not be too near to dangerous place + if (supportUnit) + { + if (gameState.ai.HQ.isNearInvadingArmy(a.position())) + aa += 50; + if (gameState.ai.HQ.isNearInvadingArmy(b.position())) + bb += 50; + } + // Give also priority to buildings with the right accessibility + const aBase = a.getMetadata(PlayerID, "base"); + const bBase = b.getMetadata(PlayerID, "base"); + if (wantedIndex) + { + if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) + aa += 30; + if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) + bb += 30; + } + // Then, if workers, small preference for bases with less workers + if (workerUnit && aBase && bBase && aBase != bBase) + { + const apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; + const bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; + if (apop > bpop) + aa++; + else if (bpop > apop) + bb++; + } + return aa - bb; + }); + } -TrainingPlan.prototype.addItem = function(amount = 1) -{ - this.number += amount; -}; + if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) + this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); + this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata); -TrainingPlan.prototype.Serialize = function() -{ - return { - "category": this.category, - "type": this.type, - "ID": this.ID, - "metadata": this.metadata, - "cost": this.cost.Serialize(), - "number": this.number, - "maxMerge": this.maxMerge - }; -}; + this.onStart(gameState); + } -TrainingPlan.prototype.Deserialize = function(gameState, data) -{ - for (const key in data) - this[key] = data[key]; + addItem(amount = 1) + { + this.number += amount; + } - this.cost = new ResourcesManager(); - this.cost.Deserialize(data.cost); -}; + Serialize() + { + return { + "category": this.category, + "type": this.type, + "ID": this.ID, + "metadata": this.metadata, + "cost": this.cost.Serialize(), + "number": this.number, + "maxMerge": this.maxMerge + }; + } + + Deserialize(gameState, data) + { + for (const key in data) + this[key] = data[key]; + + this.cost = new ResourcesManager(); + this.cost.Deserialize(data.cost); + } +}