diff --git a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index 66ed6d9071..130dd4dc7b 100644 --- a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -3,12 +3,15 @@ package com.unciv.logic.automation.city import com.unciv.GUI import com.unciv.logic.automation.Automation import com.unciv.logic.automation.civilization.NextTurnAutomation +import com.unciv.logic.automation.unit.WorkerAutomation import com.unciv.logic.city.CityConstructions import com.unciv.logic.civilization.CityAction import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PlayerType import com.unciv.logic.map.BFS +import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.IConstruction import com.unciv.models.ruleset.INonPerpetualConstruction @@ -166,36 +169,50 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { } private fun addWorkBoatChoice() { + // Does the ruleset even have "Workboats"? val buildableWorkboatUnits = units .filter { it.hasUnique(UniqueType.CreateWaterImprovements) && Automation.allowAutomatedConstruction(civInfo, city, it) }.filterBuildable() - val alreadyHasWorkBoat = buildableWorkboatUnits.any() - && !city.getTiles().any { - it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true - } - if (!alreadyHasWorkBoat) return + .toSet() + if (buildableWorkboatUnits.isEmpty()) return + // Is there already a Workboat nearby? + // todo Still ignores whether that boat can reach the not-yet-found tile to improve + val twoTurnsMovement = buildableWorkboatUnits.maxOf { (it as BaseUnit).movement } * 2 + fun MapUnit.isOurWorkBoat() = cache.hasUniqueToCreateWaterImprovements && this.civ == this@ConstructionAutomation.civInfo + val alreadyHasWorkBoat = city.getCenterTile().getTilesInDistanceRange(1..twoTurnsMovement) + .any { it.civilianUnit?.isOurWorkBoat() == true } + if (alreadyHasWorkBoat) return - val bfs = BFS(city.getCenterTile()) { - (it.isWater || it.isCityCenter()) && (it.getOwner() == null || it.isFriendlyTerritory(civInfo)) + // Define what makes a tile worth sending a Workboat to + // todo Prepare for mods that allow improving water tiles without a resource? + fun Tile.isWorthImproving(): Boolean { + if (getOwner() != civInfo) return false + if (!WorkerAutomation.hasWorkableSeaResource(this, civInfo)) return false + return WorkerAutomation.isNotBonusResourceOrWorkable(this, civInfo) } - repeat(20) { bfs.nextStep() } - if (!bfs.getReachedTiles() - .any { tile -> - tile.hasViewableResource(civInfo) && tile.improvement == null && tile.getOwner() == civInfo - && tile.tileResource.getImprovements().any { - tile.improvementFunctions.canBuildImprovement(tile.ruleset.tileImprovements[it]!!, civInfo) - } + // Search for a tile justifiying producing a Workboat + // todo should workboatAutomationSearchMaxTiles depend on game state? + fun findTileWorthImproving(): Boolean { + val searchMaxTiles = civInfo.gameInfo.ruleset.modOptions.constants.workboatAutomationSearchMaxTiles + val bfs = BFS(city.getCenterTile()) { + (it.isWater || it.isCityCenter()) + && (it.getOwner() == null || it.isFriendlyTerritory(civInfo)) + && it.isExplored(civInfo) // Sending WB's through unexplored terrain would be cheating } - ) return + do { + val tile = bfs.nextStep() ?: break + if (tile.isWorthImproving()) return true + } while (bfs.size() < searchMaxTiles) + return false + } - addChoice( - relativeCostEffectiveness, buildableWorkboatUnits.minByOrNull { it.cost }!!.name, - 0.6f - ) + if (!findTileWorthImproving()) return + + addChoice(relativeCostEffectiveness, buildableWorkboatUnits.minBy { it.cost }.name, 0.6f) } private fun addWorkerChoice() { diff --git a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index a35f9b9d0a..14d398325a 100644 --- a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -574,15 +574,9 @@ class WorkerAutomation( fun isImprovementProbablyAFort(improvement: TileImprovement): Boolean = improvement.hasUnique(UniqueType.DefensiveBonus) - private fun hasWorkableSeaResource(tile: Tile, civInfo: Civilization): Boolean = - tile.isWater && tile.improvement == null && tile.hasViewableResource(civInfo) - - private fun isNotBonusResourceOrWorkable(tile: Tile, civInfo: Civilization): Boolean = - tile.tileResource.resourceType != ResourceType.Bonus || civInfo.cities.any { it.tilesInRange.contains(tile) } - /** Try improving a Water Resource * - * No logic to avoid capture by enemies yet! + * todo: No logic to avoid capture by enemies yet! * * @return Whether any progress was made (improved a tile or at least moved towards an opportunity) */ @@ -597,13 +591,38 @@ class WorkerAutomation( .firstOrNull { unit.movement.canReach(it) && isNotBonusResourceOrWorkable(it, unit.civ) } ?: return false - // could be either fishing boats or oil well - val isImprovable = closestReachableResource.tileResource.getImprovements().any() - if (!isImprovable) return false - unit.movement.headTowards(closestReachableResource) if (unit.currentTile != closestReachableResource) return true // moving counts as progress return UnitActions.invokeUnitAction(unit, UnitActionType.CreateImprovement) } + + companion object { + // Static methods so they can be reused in ConstructionAutomation + /** Checks whether [tile] is water and has a resource [civInfo] can improve + * + * Does check whether a matching improvement can currently be built (e.g. Oil before Refrigeration). + * Can return `true` if there is an improvement that does not match the resource (for future modding abilities). + * Does not check tile ownership - caller [automateWorkBoats] already did, other callers need to ensure this explicitly. + */ + fun hasWorkableSeaResource(tile: Tile, civInfo: Civilization) = when { + !tile.isWater -> false + tile.resource == null -> false + tile.improvement != null && tile.tileResource.isImprovedBy(tile.improvement!!) -> false + !tile.hasViewableResource(civInfo) -> false + else -> tile.tileResource.getImprovements().any { + val improvement = civInfo.gameInfo.ruleset.tileImprovements[it]!! + tile.improvementFunctions.canBuildImprovement(improvement, civInfo) + } + } + + /** Test whether improving the resource on [tile] benefits [civInfo] (yields or strategic or luxury) + * + * Only tests resource type and city range, not any improvement requirements. + * @throws NullPointerException on tiles without a resource + */ + fun isNotBonusResourceOrWorkable(tile: Tile, civInfo: Civilization): Boolean = + tile.tileResource.resourceType != ResourceType.Bonus // Improve Oil even if no City reaps the yields + || civInfo.cities.any { it.tilesInRange.contains(tile) } // Improve Fish only if any of our Cities reaps the yields + } } diff --git a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt index e1e862076c..a65f5cfa45 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt @@ -248,21 +248,27 @@ class UnitMovement(val unit: MapUnit) { } /** This is performance-heavy - use as last resort, only after checking everything else! - * Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER */ - fun canReach(destination: Tile): Boolean { - if (unit.cache.cannotMove) return destination == unit.getTile() - if (unit.baseUnit.movesLikeAirUnits() || unit.isPreparingParadrop()) - return canReachInCurrentTurn(destination) - return getShortestPath(destination).any() + * Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER + * @see canReachInCurrentTurn + */ + fun canReach(destination: Tile) = canReachCommon(destination) { + getShortestPath(it).any() } - fun canReachInCurrentTurn(destination: Tile): Boolean { - if (unit.cache.cannotMove) return destination == unit.getTile() - if (unit.baseUnit.movesLikeAirUnits()) - return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits() - if (unit.isPreparingParadrop()) - return unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination) - return getDistanceToTiles().containsKey(destination) + /** Cached and thus not as performance-heavy as [canReach] */ + fun canReachInCurrentTurn(destination: Tile) = canReachCommon(destination) { + getDistanceToTiles().containsKey(it) + } + + private inline fun canReachCommon(destination: Tile, specificFunction: (Tile) -> Boolean) = when { + unit.cache.cannotMove -> + destination == unit.getTile() + unit.baseUnit.movesLikeAirUnits() -> + unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits() + unit.isPreparingParadrop() -> + unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination) + else -> + specificFunction(destination) // Note: Could pass destination as implicit closure from outer fun to lambda, but explicit is clearer } /** @@ -689,8 +695,8 @@ class UnitMovement(val unit: MapUnit) { considerZoneOfControl: Boolean = true, passThroughCache: HashMap = HashMap(), movementCostCache: HashMap, Float> = HashMap(), - includeOtherEscortUnit: Boolean = true) - : PathsToTilesWithinTurn { + includeOtherEscortUnit: Boolean = true + ): PathsToTilesWithinTurn { val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl) if (cacheResults != null) { return cacheResults diff --git a/core/src/com/unciv/models/ModConstants.kt b/core/src/com/unciv/models/ModConstants.kt index 66035a891a..ef311a3873 100644 --- a/core/src/com/unciv/models/ModConstants.kt +++ b/core/src/com/unciv/models/ModConstants.kt @@ -78,10 +78,12 @@ class ModConstants { var religionLimitBase = 1 var religionLimitMultiplier = 0.5f - //Factors in formula for pantheon cost + // Factors in formula for pantheon cost var pantheonBase = 10 var pantheonGrowth = 5 + var workboatAutomationSearchMaxTiles = 20 + fun merge(other: ModConstants) { for (field in this::class.java.declaredFields) { val value = field.get(other) diff --git a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md index 6ddea1b357..34cef3ca2b 100644 --- a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md +++ b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md @@ -202,6 +202,7 @@ and city distance in another. In case of conflicts, there is no guarantee which | religionLimitMultiplier | Float | 0.5 | [^K] | | pantheonBase | Int | 10 | [^L] | | pantheonGrowth | Int | 5 | [^L] | +| workboatAutomationSearchMaxTiles | Int | 20 | [^M] | Legend: @@ -231,6 +232,7 @@ Legend: - [^J]: A [UnitUpgradeCost](#unitupgradecost) sub-structure. - [^K]: Maximum foundable Religions = religionLimitBase + floor(MajorCivCount * religionLimitMultiplier) - [^L]: Cost of pantheon = pantheonBase + CivsWithReligion * pantheonGrowth +- [^M]: When the AI decidees whether to build a work boat, how many tiles to search from the city center for an improvable tile #### UnitUpgradeCost