diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js index cac775d773..709de6e782 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -457,6 +457,12 @@ m.Template = m.Class({ return this.get("UnitMotion/WalkSpeed"); }, + trainingCategory: function() { + if (!this.get("TrainingRestrictions") || !this.get("TrainingRestrictions/Category")) + return undefined; + return this.get("TrainingRestrictions/Category"); + }, + buildCategory: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Category")) return undefined; diff --git a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js index b87fbf1ae0..fa654c6be5 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js @@ -593,14 +593,8 @@ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) if (!okay) continue; - for (let limitedClass in limits) - { - if (!template.hasClass(limitedClass) || current[limitedClass] < limits[limitedClass]) - continue; - okay = false; - break; - } - if (!okay) + let category = template.trainingCategory(); + if (category && limits[category] && current[category] >= limits[category]) continue; ret.push( [trainable, template] ); diff --git a/binaries/data/mods/public/simulation/ai/petra/baseManager.js b/binaries/data/mods/public/simulation/ai/petra/baseManager.js index 5ae7ce5961..a49bbf6dd9 100644 --- a/binaries/data/mods/public/simulation/ai/petra/baseManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/baseManager.js @@ -409,24 +409,27 @@ m.BaseManager.prototype.checkResourceLevels = function (gameState, queues) { if (type == "food") { - var count = this.getResourceLevel(gameState, type); // TODO animals are not accounted, may-be we should - var numFarms = gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true); - var numFound = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); - var numQueue = queues.field.countQueuedUnits(); + if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. + { + var count = this.getResourceLevel(gameState, type); // TODO animals are not accounted, may-be we should + var numFarms = gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true); + var numFound = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); + var numQueue = queues.field.countQueuedUnits(); - // TODO if not yet farms, add a check on time used/lost and build farmstead if needed - if (numFarms + numFound + numQueue === 0) // starting game, rely on fruits as long as we have enough of them - { - if (count < 600) - queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); - } - else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. - { - let goal = this.Config.Economy.provisionFields; - if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace) - goal = Math.max(goal-1, 1); - if (numFound + numQueue < goal) - queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + // TODO if not yet farms, add a check on time used/lost and build farmstead if needed + if (numFarms + numFound + numQueue === 0) // starting game, rely on fruits as long as we have enough of them + { + if (count < 600) + queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + } + else + { + let goal = this.Config.Economy.provisionFields; + if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace) + goal = Math.max(goal-1, 1); + if (numFound + numQueue < goal) + queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + } } } else if (queues.dropsites.length() == 0 && gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_storehouse"), true) == 0) diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index 856d3b13c9..df7b829311 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -530,7 +530,7 @@ m.HQ.prototype.pickMostNeededResources = function(gameState) // 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, fromStrategic) +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) @@ -555,6 +555,20 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, var bestIdx = undefined; var bestVal = undefined; var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); + var scale = 250 * 250; + var proxyAccess = undefined; + var nbShips = this.navalManager.transportShips.length; + if (proximity) // this is our first base + { + // if our first base, ensure room around + radius = Math.ceil((template.obstructionRadius() + 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 + var cellArea = gameState.getMap().cellSize * gameState.getMap().cellSize; + proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); + if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) + scale = 400 * 400; + } var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; @@ -567,6 +581,8 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, var index = gameState.ai.accessibility.landPassMap[j]; if (!this.landRegions[index]) continue; + if (proxyAccess && nbShips === 0 && proxyAccess !== index) + continue; // and with enough room around to build the cc var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions); if (obstructions.map[i] <= radius) @@ -575,61 +591,71 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, var norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // checking distance to other cc var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - var minDist = Math.min(); - for (var cc of ccList) + if (proximity) // this is our first cc, let's do it near our units { - var dist = API3.SquareVectorDistance(cc.pos, pos); - if (dist < 14000) // Reject if too near from any cc - { - norm = 0 - break; - } - if (!cc.ally) - continue; - if (dist < 30000) // Reject if too near from an allied cc - { - norm = 0 - break; - } - if (dist < 50000) // Disfavor if quite near an allied cc - norm *= 0.5; - if (dist < minDist) - minDist = dist; + var dist = API3.SquareVectorDistance(proximity, pos); + norm /= (1 + dist/scale); } - if (norm == 0) - continue; - if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (-> not connected) + else { - norm = 0; - continue; - } - else if (minDist > 130000) // Disfavor if quite far from any allied cc - { - if (this.navalMap) + var minDist = Math.min(); + + for (var cc of ccList) { - if (minDist > 250000) + var dist = API3.SquareVectorDistance(cc.pos, pos); + if (dist < 14000) // Reject if too near from any cc + { + norm = 0 + break; + } + if (!cc.ally) + continue; + if (dist < 30000) // Reject if too near from an allied cc + { + norm = 0 + break; + } + if (dist < 50000) // Disfavor if quite near an allied cc norm *= 0.5; - else - norm *= 0.8; + if (dist < minDist) + minDist = dist; } - else - norm *= 0.5; - } + if (norm == 0) + continue; - for (var dp of dpList) - { - var dist = API3.SquareVectorDistance(dp.pos, pos); - if (dist < 3600) + if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (not connected) { norm = 0; - break; + continue; } - else if (dist < 6400) - norm *= 0.5; + else if (minDist > 130000) // Disfavor if quite far from any allied cc + { + if (this.navalMap) + { + if (minDist > 250000) + norm *= 0.5; + else + norm *= 0.8; + } + else + norm *= 0.5; + } + + for (var dp of dpList) + { + var dist = API3.SquareVectorDistance(dp.pos, pos); + if (dist < 3600) + { + norm = 0; + break; + } + else if (dist < 6400) + norm *= 0.5; + } + if (norm == 0) + continue; } - if (norm == 0) - continue; if (this.borderMap.map[j] > 0) // disfavor the borders of the map norm *= 0.5; @@ -649,7 +675,7 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, Engine.ProfileStop(); var cut = 60; - if (fromStrategic) // be less restrictive + if (fromStrategic || proximity) // be less restrictive cut = 30; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); @@ -695,7 +721,7 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) ++numAllyCC; } if (numAllyCC < 2) - return this.findEconomicCCLocation(gameState, template, "wood", true); + return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); @@ -1149,10 +1175,10 @@ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.length() > 0) return; - // first build one cc if none already available + // first build one cc if all have been destroyed if (this.numActiveBase() < 1) { - this.buildNewBase(gameState, queues); + this.buildFirstBase(gameState); return; } // then expand if we have not enough room available for buildings @@ -1176,7 +1202,7 @@ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) } }; -m.HQ.prototype.buildNewBase = function(gameState, queues, type) +m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.numActiveBase() > 0 && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase())) return false; @@ -1188,8 +1214,8 @@ m.HQ.prototype.buildNewBase = function(gameState, queues, type) // base "-1" means new base. if (this.Config.debug > 1) - API3.warn("new base planned with type " + type); - queues.civilCentre.addItem(new m.ConstructionPlan(gameState, template, { "base": -1, "type": type })); + API3.warn("new base planned with resource " + resource); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; @@ -1520,9 +1546,9 @@ m.HQ.prototype.canBuild = function(gameState, structure) if (!template || !template.available(gameState)) return false; var limits = gameState.getEntityLimits(); - for (var limitedClass in limits) - if (template.hasClass(limitedClass) && gameState.getEntityCounts()[limitedClass] >= limits[limitedClass]) - return false; + var category = template.buildCategory(); + if (category && limits[category] && gameState.getEntityCounts()[category] >= limits[category]) + return false; return true; }; diff --git a/binaries/data/mods/public/simulation/ai/petra/navalManager.js b/binaries/data/mods/public/simulation/ai/petra/navalManager.js index dac1cbb21c..836e5d3ed3 100644 --- a/binaries/data/mods/public/simulation/ai/petra/navalManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/navalManager.js @@ -388,6 +388,37 @@ m.NavalManager.prototype.splitTransport = function(gameState, plan) return (nbUnits !== 0); }; +/** + * create a transport from a garrisoned ship to a land location + * needed at start game when starting with a garrisoned ship + */ +m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos) +{ + let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); + if (fromAccess !== 1) + return; + let toAccess = gameState.ai.accessibility.getAccessValue(toPos); + if (toAccess < 2) + return; + + for (let ship of this.ships.values()) + { + if (!ship.isGarrisonHolder() || !ship.garrisoned().length) + continue; + if (ship.getMetadata(PlayerID, "transporter") !== undefined) + continue; + let units = []; + for (let entId of ship.garrisoned()) + units.push(gameState.getEntityById(entId)); + // TODO check that the garrisoned units have not another purpose + let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); + if (plan.failed) + continue; + plan.init(gameState); + this.transportPlans.push(plan); + } +}; + // set minimal number of needed ships when a new event (new base or new attack plan) m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) { @@ -606,15 +637,8 @@ m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) if (!template.available(gameState)) continue; - var aboveLimit = false; - for (var limitedClass in limits) - { - if (!template.hasClass(limitedClass) || current[limitedClass] < limits[limitedClass]) - continue; - aboveLimit = true; - break; - } - if (aboveLimit) + var category = template.trainingCategory(); + if (category && limits[category] && current[category] >= limits[category]) continue; var arrows = +(template.getDefaultArrow() || 0); @@ -649,7 +673,7 @@ m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) m.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); - + this.checkEvents(gameState, queues, events); // close previous transport plans if finished diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js b/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js index 7122f7200d..37c2dafc65 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js @@ -86,6 +86,10 @@ m.ConstructionPlan.prototype.start = function(gameState) } this.onStart(gameState); Engine.ProfileStop(); + + // TODO should have a ConstructionStarted event in case the construct order fails + if (this.metadata && this.metadata.proximity) + gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z]); }; // TODO for dock, we should allow building them outside territory, and we should check that we are along the right sea @@ -101,8 +105,11 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { if (template.hasClass("CivCentre")) { - if (this.metadata.type) - var pos = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.type); + if (this.metadata.resource) + { + var proximity = this.metadata.proximity ? this.metadata.proximity : undefined; + var pos = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); + } else var pos = gameState.ai.HQ.findStrategicCCLocation(gameState, template); diff --git a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js index 08a3a9b001..3e586fc7b9 100644 --- a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js +++ b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -9,6 +9,10 @@ m.HQ.prototype.gameAnalysis = function(gameState) // Analysis of the terrain and the different access regions this.regionAnalysis(gameState); + this.attackManager.init(gameState); + this.navalManager.init(gameState); + this.tradeManager.init(gameState); + // Make a list of buildable structures from the config file this.structureAnalysis(gameState); @@ -30,7 +34,7 @@ m.HQ.prototype.gameAnalysis = function(gameState) // Assign entities and resources in the different bases this.assignStartingEntities(gameState); - // Check if we will ever be able to produce units + // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks this.canBuildUnits = true; if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).length) { @@ -42,12 +46,10 @@ m.HQ.prototype.gameAnalysis = function(gameState) this.canBuildUnits = false; this.dispatchUnits(gameState); } + else + this.buildFirstBase(gameState); } - this.attackManager.init(gameState); - this.navalManager.init(gameState); - this.tradeManager.init(gameState); - // configure our first base strategy if (this.baseManagers.length > 1) this.configFirstBase(gameState); @@ -106,7 +108,7 @@ m.HQ.prototype.assignStartingEntities = function(gameState) if (!bestbase) { // entity outside our territory - var bestbase = m.getBestBase(ent, gameState); + bestbase = m.getBestBase(ent, gameState); bestbase.assignEntity(ent); if (bestbase.ID !== this.baseManagers[0].ID && ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) bestbase.assignResourceToDropsite(gameState, ent); @@ -114,7 +116,7 @@ m.HQ.prototype.assignStartingEntities = function(gameState) // now assign entities garrisoned inside this entity if (ent.isGarrisonHolder() && ent.garrisoned().length) for (let id of ent.garrisoned()) - bestBase.assignEntity(gameState.getEntityByID(id)); + bestbase.assignEntity(gameState.getEntityById(id)); } }; @@ -226,6 +228,79 @@ m.HQ.prototype.structureAnalysis = function(gameState) this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); }; +/** + * build our first base + */ +m.HQ.prototype.buildFirstBase = function(gameState) +{ + var total = gameState.getResources(); + var template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_civil_centre")); + if (!total.canAfford(new API3.Resources(template.cost()))) + { +/* API3.warn("not enough resource to build a cc, try with a dock"); + template = gameState.applyCiv("structures/{civ}_dock"); + if (!gameState.isDisabledTemplates(template)) + { + template = gameState.getTemplate(template); + if (!total.canAfford(new API3.Resources(template.cost()))) + { + API3.warn("not enough resource for dock ... return"); + return; + } + API3.warn("but we could still build a dock if it was implemented"); + return; + } */ + return; + } + + // We first choose as startingPoint the point where we have the more units + let startingPoint = []; + for (let ent of gameState.getOwnUnits().values()) + { + if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) + continue; + if (ent.hasClass("Cavalry")) + continue; + let pos = ent.position(); + if (!pos) + { + let holder = m.getHolder(ent, gameState); + if (!holder || !holder.position()) + continue; + pos = holder.position(); + } + let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); + let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; + let land = gameState.ai.accessibility.landPassMap[index]; + let sea = gameState.ai.accessibility.navalPassMap[index]; + let found = false; + for (let point of startingPoint) + { + if (land !== point.land || sea !== point.sea) + continue; + if (API3.SquareVectorDistance(point.pos, pos) > 2500) + continue; + point.weight += 1; + found = true; + break; + } + if (!found) + startingPoint.push({"pos": pos, "land": land, "sea": sea, "weight": 1}); + } + if (!startingPoint.length) + { + API3.warn("Petra error in buildFirstBase, can not find a starting position"); + return; + } + let imax = 0; + for (let i = 1; i < startingPoint.length; ++i) + if (startingPoint[i].weight > startingPoint[imax].weight) + imax = i; + + var template = gameState.applyCiv("structures/{civ}_civil_centre"); + gameState.ai.queues.civilCentre.addItem(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); +}; + /** * set strategy if game without construction: * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack diff --git a/binaries/data/mods/public/simulation/ai/petra/transportPlan.js b/binaries/data/mods/public/simulation/ai/petra/transportPlan.js index a51d274dab..2765510572 100644 --- a/binaries/data/mods/public/simulation/ai/petra/transportPlan.js +++ b/binaries/data/mods/public/simulation/ai/petra/transportPlan.js @@ -23,7 +23,7 @@ var PETRA = function(m) transporter = this.ID */ -m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos) +m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) { this.ID = gameState.ai.uniqueIDs.transports++; this.debug = gameState.ai.Config.debug; @@ -34,16 +34,32 @@ m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos) this.startIndex = startIndex; // TODO only cases with land-sea-land are allowed for the moment // we could also have land-sea-land-sea-land - this.sea = gameState.ai.HQ.getSeaIndex(gameState, startIndex, endIndex); - if (!this.sea) + if (startIndex === 1) { - this.failed = true; - if (this.debug > 1) - API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); - return false; + // special transport from already garrisoned ship + if (!ship) + { + this.failed = true; + return false; + } + this.sea = ship.getMetadata(PlayerID, "sea"); + ship.setMetadata(PlayerID, "transporter", this.ID); + for (let ent of units) + ent.setMetadata(PlayerID, "onBoard", "onBoard"); + } + else + { + this.sea = gameState.ai.HQ.getSeaIndex(gameState, startIndex, endIndex); + if (!this.sea) + { + this.failed = true; + if (this.debug > 1) + API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); + return false; + } } - for (var ent of units) + for (let ent of units) { ent.setMetadata(PlayerID, "transport", this.ID); ent.setMetadata(PlayerID, "endPos", endPos); @@ -55,7 +71,7 @@ m.TransportPlan = function(gameState, units, startIndex, endIndex, endPos) this.state = "boarding"; this.boardingPos = {}; - this.needTransportShips = true; + this.needTransportShips = (ship === undefined); this.nTry = {}; return true; };