Forts and citadels (with AI) (#2325)

* Enabled Forts & Citadels

* Friendly territory checks

* Citadel damage & notifications

* Sprites, Icons, Translation & Atlas

* Obsolete tests are removed

* NullReferenceException code is fixed

* Refactoring: using the static object

* AI for the forts and citadels

* Display defence stats

* Exclude enemies tiles as candidates

Co-authored-by: r3versi <fluo392@gmail.com>
This commit is contained in:
Jack Rainy 2020-04-03 11:22:27 +03:00 committed by GitHub
parent 10762a3873
commit 29a077a803
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 197 additions and 111 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

View file

@ -79,7 +79,16 @@
"improvingTech": "Compass",
"improvingTechStats": {"gold": 1}
},
// Military improvement
{
name: "Fort",
terrainsCanBeBuiltOn: ["Plains","Grassland","Desert","Hill","Tundra","Snow"],
turnsToBuild: 6,
techRequired: "Engineering",
uniques: ["Gives a defensive bonus of 50%"]
},
// Transportation
{
"name": "Road",
@ -151,7 +160,12 @@
"improvingTech": "Economics",
"improvingTechStats": {"gold": 1}
},
{
name: "Citadel",
uniques: ["Gives a defensive bonus of 100%", "Deal 30 damage to adjacent enemy units"]
// TODO (G&K): adds every tile around it to your territory
},
//Civilization unique improvements
{
"name": "Moai",

View file

@ -1324,8 +1324,7 @@
"name": "Great General",
"unbuildable": true,
"unitType": "Civilian",
"uniques": ["Can start an 8-turn golden age","Bonus for units in 2 tile radius 15%"],
//todo : should be able to build mega-fort
"uniques": ["Can start an 8-turn golden age","Bonus for units in 2 tile radius 15%", "Can build improvement: Citadel"],
"movement": 2
},
{
@ -1334,8 +1333,7 @@
"unitType": "Civilian",
"uniqueTo": "Mongolia",
"replaces": "Great General",
"uniques": ["Can start an 8-turn golden age","Bonus for units in 2 tile radius 15%", "Heal adjacent units for an additional 15 HP per turn"],
//todo : should be able to build mega-fort
"uniques": ["Can start an 8-turn golden age","Bonus for units in 2 tile radius 15%", "Heal adjacent units for an additional 15 HP per turn", "Can build improvement: Citadel"],
"movement": 5
}
]

View file

@ -1,4 +1,4 @@

# Tutorial tasks
Move a unit!\nClick on a unit > Click on a destination > Click the arrow popup = Sposta un'unità!\nClicca su un'unità > Clicca su una destinazione > Clicca sul popup con la freccia

View file

@ -44,7 +44,7 @@ object Constants {
const val researchAgreement = "Research Agreement"
const val openBorders = "Open Borders"
const val random = "Random"
val greatImprovements = listOf("Academy", "Landmark", "Manufactory", "Customs house")
val greatImprovements = listOf("Academy", "Landmark", "Manufactory", "Customs house", "Citadel")
val unitActionSetUp = "Set Up"
val unitActionSleep = "Sleep"

View file

@ -212,7 +212,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
// If this city is the closest city to another civ, that makes it a likely candidate for attack
if (civInfo.getKnownCivs().filter { it.cities.isNotEmpty() }
.any { NextTurnAutomation().getClosestCities(civInfo, it).city1 == cityInfo })
.any { NextTurnAutomation.getClosestCities(civInfo, it).city1 == cityInfo })
modifier *= 1.5f
addChoice(relativeCostEffectiveness, defensiveBuilding.name, modifier)

View file

@ -282,21 +282,6 @@ class NextTurnAutomation{
}
}
fun getMinDistanceBetweenCities(civ1: CivilizationInfo, civ2: CivilizationInfo): Int {
return getClosestCities(civ1,civ2).aerialDistance
}
data class CityDistance(val city1:CityInfo, val city2:CityInfo, val aerialDistance: Int)
fun getClosestCities(civ1: CivilizationInfo, civ2: CivilizationInfo): CityDistance {
val cityDistances = arrayListOf<CityDistance>()
for (civ1city in civ1.cities)
for (civ2city in civ2.cities)
cityDistances.add(CityDistance(civ1city, civ2city,
civ1city.getCenterTile().aerialDistanceTo(civ2city.getCenterTile())))
return cityDistances.minBy { it.aerialDistance }!!
}
private fun offerDeclarationOfFriendship(civInfo: CivilizationInfo) {
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
.asSequence()
@ -509,4 +494,22 @@ class NextTurnAutomation{
diplomacyManager.removeFlag(DiplomacyFlags.SettledCitiesNearUs)
}
companion object
{
fun getMinDistanceBetweenCities(civ1: CivilizationInfo, civ2: CivilizationInfo): Int {
return getClosestCities(civ1,civ2).aerialDistance
}
data class CityDistance(val city1:CityInfo, val city2:CityInfo, val aerialDistance: Int)
fun getClosestCities(civ1: CivilizationInfo, civ2: CivilizationInfo): CityDistance {
val cityDistances = arrayListOf<CityDistance>()
for (civ1city in civ1.cities)
for (civ2city in civ2.cities)
cityDistances.add(CityDistance(civ1city, civ2city,
civ1city.getCenterTile().aerialDistanceTo(civ2city.getCenterTile())))
return cityDistances.minBy { it.aerialDistance }!!
}
}
}

View file

@ -61,16 +61,27 @@ class SpecificUnitAutomation {
return
}
//if no unit to follow, take refuge in city.
// try to build a citadel
if (WorkerAutomation(unit).evaluateFortPlacement(unit.currentTile, unit.civInfo))
UnitActions.getGreatPersonBuildImprovementAction(unit)?.action?.invoke()
//if no unit to follow, take refuge in city or build citadel there.
val reachableTest : (TileInfo) -> Boolean = {it.civilianUnit == null &&
unit.movement.canMoveTo(it)
&& unit.movement.canReach(it)}
val cityToGarrison = unit.civInfo.cities.asSequence().map { it.getCenterTile() }
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull {
it.civilianUnit == null && unit.movement.canMoveTo(it)
&& unit.movement.canReach(it)
}
.firstOrNull { reachableTest(it) }
if (cityToGarrison != null) {
unit.movement.headTowards(cityToGarrison)
// try to find a good place for citadel nearby
val potentialTilesNearCity = cityToGarrison.getTilesInDistanceRange(3..4)
val tileForCitadel = potentialTilesNearCity.firstOrNull { reachableTest(it) &&
WorkerAutomation(unit).evaluateFortPlacement(it, unit.civInfo) }
if (tileForCitadel != null)
unit.movement.headTowards(tileForCitadel)
else
unit.movement.headTowards(cityToGarrison)
return
}
}

View file

@ -191,6 +191,8 @@ class WorkerAutomation(val unit: MapUnit) {
tile.containsGreatImprovement() -> null
tile.containsUnfinishedGreatImprovement() -> null
// Defence is more important that civilian improvements
evaluateFortPlacement(tile,civInfo) -> "Fort"
// I think we can assume that the unique improvement is better
uniqueImprovement!=null && tile.canBuildImprovement(uniqueImprovement,civInfo) -> uniqueImprovement.name
@ -208,4 +210,73 @@ class WorkerAutomation(val unit: MapUnit) {
return unit.civInfo.gameInfo.ruleSet.tileImprovements[improvementString]!!
}
private fun isAcceptableTileForFort(tile: TileInfo, civInfo: CivilizationInfo): Boolean
{
// don't build fort in the city
if (tile.isCityCenter()) return false
// don't build fort if it is already here
if (tile.improvement == "Fort") return false
// don't build on resource tiles
if (tile.hasViewableResource(civInfo)) return false
// don't build on great improvements
if (tile.containsGreatImprovement() || tile.containsUnfinishedGreatImprovement()) return false
return true
}
fun evaluateFortPlacement(tile: TileInfo, civInfo: CivilizationInfo): Boolean {
// build on our land only
if ((tile.owningCity?.civInfo != civInfo) ||
!isAcceptableTileForFort(tile, civInfo)) return false
val isHills = tile.getBaseTerrain().name == Constants.hill
// if this place is not perfect, let's see if there is a better one
val nearestTiles = tile.getTilesInDistance(2).filter{it.owningCity?.civInfo == civInfo}.toList()
for (closeTile in nearestTiles) {
// don't build forts too close to the cities
if (closeTile.isCityCenter()) return false
// don't build forts too close to other forts
if (closeTile.improvement == "Fort" || closeTile.improvement == "Citadel"
|| closeTile.improvementInProgress == "Fort") return false
// there is another better tile for the fort
if (!isHills && tile.getBaseTerrain().name == Constants.hill &&
isAcceptableTileForFort(closeTile, civInfo)) return false
}
val enemyCivs = civInfo.getKnownCivs()
.filterNot { it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canAttack() }
// no potential enemies
if (enemyCivs.isEmpty()) return false
val threatMapping : (CivilizationInfo) -> Int = {
// the war is already a good nudge to build forts
(if (civInfo.isAtWarWith(it)) 20 else 0) +
// let's check also the force of the enemy
when (Automation().threatAssessment(civInfo, it)) {
ThreatLevel.VeryLow -> 1 // do not build forts
ThreatLevel.Low -> 6 // too close, let's build until it is late
ThreatLevel.Medium -> 10
ThreatLevel.High -> 15 // they are strong, let's built until they reach us
ThreatLevel.VeryHigh -> 20
} }
val enemyCivsIsCloseEnough = enemyCivs.filter { NextTurnAutomation.getMinDistanceBetweenCities(civInfo, it) <= threatMapping(it) }
// no threat, let's not build fort
if (enemyCivsIsCloseEnough.isEmpty()) return false
// make list of enemy cities as sources of threat
val enemyCities = mutableListOf<TileInfo>()
enemyCivsIsCloseEnough.forEach { enemyCities.addAll(it.cities.map { city -> city.getCenterTile() } ) }
// find closest enemy city
val closestEnemyCity = enemyCities.minBy { it.aerialDistanceTo(tile) }!!
val distanceToEnemy = tile.aerialDistanceTo(closestEnemyCity)
// find closest our city to defend from this enemy city
val closestOurCity = tile.owningCity!!.getCenterTile()
val distanceBetweenCities = closestEnemyCity.aerialDistanceTo(closestOurCity)
// let's build fort on the front line, not behind the city
return distanceBetweenCities > distanceToEnemy
}
}

View file

@ -160,6 +160,7 @@ class BattleDamage{
fun getDefenceModifiers(attacker: ICombatant, defender: MapUnitCombatant): HashMap<String, Float> {
val modifiers = HashMap<String, Float>()
val tile = defender.getTile()
if (defender.unit.isEmbarked()) {
// embarked units get no defensive modifiers apart from this unique
@ -172,11 +173,17 @@ class BattleDamage{
modifiers.putAll(getGeneralModifiers(defender, attacker))
modifiers.putAll(getTileSpecificModifiers(defender, defender.getTile()))
modifiers.putAll(getTileSpecificModifiers(defender, tile))
if (!defender.unit.hasUnique("No defensive terrain bonus")) {
val tileDefenceBonus = defender.getTile().getDefensiveBonus()
if (tileDefenceBonus > 0) modifiers["Terrain"] = tileDefenceBonus
val tileDefenceBonus = tile.getDefensiveBonus()
if (tileDefenceBonus > 0)
modifiers["Terrain"] = tileDefenceBonus
val improvement = tile.getTileImprovement()
if (improvement != null && tile.isFriendlyTerritory(defender.getCivInfo()))
if (improvement.hasUnique("Gives a defensive bonus of 50%")) modifiers[improvement.name] = 0.50f
else if (improvement.hasUnique("Gives a defensive bonus of 100%")) modifiers[improvement.name] = 1.0f
}
if(attacker.isRanged()) {
@ -196,10 +203,9 @@ class BattleDamage{
private fun getTileSpecificModifiers(unit: MapUnitCombatant, tile: TileInfo): HashMap<String,Float> {
val modifiers = HashMap<String,Float>()
val isFriendlyTerritory = tile.getOwner()!=null && !unit.getCivInfo().isAtWarWith(tile.getOwner()!!)
if(isFriendlyTerritory && unit.getCivInfo().containsBuildingUnique("+15% combat strength for units fighting in friendly territory"))
if(tile.isFriendlyTerritory(unit.getCivInfo()) && unit.getCivInfo().containsBuildingUnique("+15% combat strength for units fighting in friendly territory"))
modifiers["Himeji Castle"] = 0.15f
if(!isFriendlyTerritory && unit.unit.hasUnique("+20% bonus outside friendly territory"))
if(!tile.isFriendlyTerritory(unit.getCivInfo()) && unit.unit.hasUnique("+20% bonus outside friendly territory"))
modifiers["Foreign Land"] = 0.2f

View file

@ -527,7 +527,7 @@ class CivilizationInfo {
}
fun giftMilitaryUnitTo(otherCiv: CivilizationInfo) {
val city = NextTurnAutomation().getClosestCities(this, otherCiv).city1
val city = NextTurnAutomation.getClosestCities(this, otherCiv).city1
val militaryUnit = city.cityConstructions.getConstructableUnits()
.filter { !it.unitType.isCivilian() && it.unitType.isLandUnit() }
.toList().random()

View file

@ -235,7 +235,7 @@ class DiplomacyManager() {
*
* This includes friendly and allied city-states and the open border treaties.
*/
fun isConsideredAllyTerritory(): Boolean {
fun isConsideredFriendlyTerritory(): Boolean {
if(civInfo.isCityState() && relationshipLevel() >= RelationshipLevel.Friend)
return true
return hasOpenBorders

View file

@ -411,25 +411,19 @@ class MapUnit {
/** Returns the health points [MapUnit] will receive if healing on [tileInfo] */
fun rankTileForHealing(tileInfo: TileInfo): Int {
val tileOwner = tileInfo.getOwner()
val isAlliedTerritory = when {
tileOwner == null -> false
tileOwner == civInfo -> true
!civInfo.knows(tileOwner) -> false
else -> tileOwner.getDiplomacyManager(civInfo).isConsideredAllyTerritory()
}
val isFriendlyTerritory = tileInfo.isFriendlyTerritory(civInfo)
var healing = when {
tileInfo.isCityCenter() -> 20
tileInfo.isWater && isAlliedTerritory && type.isWaterUnit() -> 15 // Water unit on friendly water
tileInfo.isWater && isFriendlyTerritory && type.isWaterUnit() -> 15 // Water unit on friendly water
tileInfo.isWater -> 0 // All other water cases
tileOwner == null -> 10 // Neutral territory
isAlliedTerritory -> 15 // Allied territory
tileInfo.getOwner() == null -> 10 // Neutral territory
isFriendlyTerritory -> 15 // Allied territory
else -> 5 // Enemy territory
}
if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.")
&& !isAlliedTerritory
&& !isFriendlyTerritory
// Additional healing from medic is only applied when the unit is able to heal
&& healing > 0)
healing += 5
@ -439,7 +433,7 @@ class MapUnit {
fun endTurn() {
doPostTurnAction()
if(currentMovement== getMaxMovement().toFloat() // didn't move this turn
if (currentMovement == getMaxMovement().toFloat() // didn't move this turn
|| getUniques().contains("Unit will heal every turn, even if it performs an action")){
heal()
}
@ -447,6 +441,8 @@ class MapUnit {
if (action!!.endsWith(" until healed")) {
action = null // wake up when healed
}
getCitadelDamage()
}
fun startTurn() {
@ -662,5 +658,25 @@ class MapUnit {
return sum
}
private fun getCitadelDamage() {
// Check for Citadel damage
val applyCitadelDamage = currentTile.neighbors
.filter{ it.getOwner() != null && civInfo.isAtWarWith(it.getOwner()!!) }
.map{ it.getTileImprovement() }
.filter{ it != null && it.hasUnique("Deal 30 damage to adjacent enemy units") }
.any()
if (applyCitadelDamage) {
health -= 30
if (health <= 0) {
civInfo.addNotification("An enemy [Citadel] has destroyed our [$name]", currentTile.position, Color.RED)
destroy()
} else {
civInfo.addNotification("An enemy [Citadel] has attacked our [$name]", currentTile.position, Color.RED)
}
}
}
//endregion
}

View file

@ -1,4 +1,4 @@
package com.unciv.logic.map
package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
@ -132,9 +132,20 @@ open class TileInfo {
return containingCity.civInfo
}
fun isFriendlyTerritory(civInfo: CivilizationInfo): Boolean {
val tileOwner = getOwner()
return when {
tileOwner == null -> false
tileOwner == civInfo -> true
!civInfo.knows(tileOwner) -> false
else -> tileOwner.getDiplomacyManager(civInfo).isConsideredFriendlyTerritory()
}
}
fun getTerrainFeature(): Terrain? =
if (terrainFeature == null) null else ruleset.terrains[terrainFeature!!]
fun isWorked(): Boolean {
val city = getCity()
return city!=null && city.workedTiles.contains(position)
@ -337,8 +348,17 @@ open class TileInfo {
milUnitString += " - "+militaryUnit!!.civInfo.civName.tr()
lineList += milUnitString
}
if(getDefensiveBonus()!=0f){
var defencePercentString = (getDefensiveBonus()*100).toInt().toString()+"%"
var defenceBonus = getDefensiveBonus()
val tileImprovement = getTileImprovement()
if (tileImprovement != null) {
defenceBonus += when {
tileImprovement.hasUnique("Gives a defensive bonus of 50%") -> 0.5f
tileImprovement.hasUnique("Gives a defensive bonus of 100%") -> 1.0f
else -> 0.0f
}
}
if(defenceBonus != 0.0f){
var defencePercentString = (defenceBonus*100).toInt().toString()+"%"
if(!defencePercentString.startsWith("-")) defencePercentString = "+$defencePercentString"
lineList += "[$defencePercentString] to unit defence".tr()
}

View file

@ -64,5 +64,7 @@ class TileImprovement : NamedStats() {
return stringBuilder.toString()
}
fun hasUnique(unique: String) = uniques.contains(unique)
}

View file

@ -40,6 +40,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table(){
"Construct Academy" -> return ImageGetter.getImprovementIcon("Academy")
"Start Golden Age" -> return ImageGetter.getUnitIcon("Great Artist")
"Construct Landmark" -> return ImageGetter.getImprovementIcon("Landmark")
"Construct Citadel" -> return ImageGetter.getImprovementIcon("Citadel")
"Hurry Wonder" -> return ImageGetter.getUnitIcon("Great Engineer")
"Construct Manufactory" -> return ImageGetter.getImprovementIcon("Manufactory")
"Conduct Trade Mission" -> return ImageGetter.getUnitIcon("Great Merchant")

View file

@ -153,6 +153,8 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Ruins](https://thenounproject.com/term/ruins/3849/) By Paulo Volkova for City ruins
* [Fishing Net](https://thenounproject.com/term/fishing-net/1073133/) By Made for Fishing Boats
* [Moai](https://thenounproject.com/search/?q=moai&i=2878111) By Template
* [Fort](https://thenounproject.com/term/fort/1697645/) By Adrien Coquet
* [Citadel](https://thenounproject.com/term/fort/1697646/) By Adrien Coquet
## Buildings

View file

@ -45,64 +45,6 @@ class TranslationTests {
allUnitActionsHaveTranslation)
}
@Test
fun allTerrainsHaveTranslation() {
val strings: Set<String> = ruleset.terrains.keys
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all buildings",
allStringsHaveTranslation)
}
@Test
fun allTerrainUniquesHaveTranslation() {
val strings: MutableSet<String> = HashSet()
for (terrain in ruleset.terrains.values) {
strings.addAll(terrain.uniques)
}
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all terrain uniques",
allStringsHaveTranslation)
}
@Test
fun allImprovementsHaveTranslation() {
val strings: Set<String> = ruleset.tileImprovements.keys
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all improvements",
allStringsHaveTranslation)
}
@Test
fun allImprovementUniquesHaveTranslation() {
val strings: MutableSet<String> = HashSet()
for (improvement in ruleset.tileImprovements.values) {
strings.addAll(improvement.uniques)
}
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all improvements uniques",
allStringsHaveTranslation)
}
@Test
fun allTechnologiesHaveTranslation() {
val strings: Set<String> = ruleset.technologies.keys
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all technologies",
allStringsHaveTranslation)
}
@Test
fun allTechnologiesQuotesHaveTranslation() {
val strings: MutableSet<String> = HashSet()
for (tech in ruleset.technologies.values) {
strings.add(tech.quote)
}
val allStringsHaveTranslation = allStringAreTranslated(strings)
Assert.assertTrue("This test will only pass when there is a translation for all technologies quotes",
allStringsHaveTranslation)
}
private fun allStringAreTranslated(strings: Set<String>): Boolean {
var allStringsHaveTranslation = true
for (key in strings) {