mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-07 00:41:39 +07:00
Work boat construction automation tweaks (#11395)
* Minor lint and optimize addWorkBoatChoice * Moddable findTileWorthImproving search distance * Don't count bonus resources outside any city work range as worth improving * Look for existing work boat in a fixed radius instead of city-owned tiles, depending on work boat speed * Some UnitMovement readability * Work boat construction and automation code synergies
This commit is contained in:
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<Tile, Boolean> = HashMap(),
|
||||
movementCostCache: HashMap<Pair<Tile, Tile>, Float> = HashMap(),
|
||||
includeOtherEscortUnit: Boolean = true)
|
||||
: PathsToTilesWithinTurn {
|
||||
includeOtherEscortUnit: Boolean = true
|
||||
): PathsToTilesWithinTurn {
|
||||
val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl)
|
||||
if (cacheResults != null) {
|
||||
return cacheResults
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user