Carthage civ (#5224)

* Add Carthage

* Implement uniques

* performance improvement, better elephant

* AI avoids taking too much damage from mountains

* more performance

* better AI

* can't settle cities on mountains

* AI improvement

* AI improvement

* revisions, damagePerTurn in Terrains.json

* terrain damage stored as unique in json, damage also works for terrain features

* don't change game.png
This commit is contained in:
SimonCeder
2021-09-18 19:28:12 +02:00
committed by GitHub
parent 7e05a56e37
commit 8eb24ac273
15 changed files with 165 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -773,6 +773,33 @@
"Kapfenberg", "Hallein", "Bischofshofen", "Waidhofen", "Saalbach", "Lienz", "Steyr"
]
},
{
"name": "Carthage",
"leaderName": "Dido",
"adjective": ["Carthaginian"],
"startBias": ["Coast"],
"preferredVictoryType": "Domination",
"startIntroPart1": "Blessings and salutations to you, revered Queen Dido, founder of the legendary kingdom of Carthage. Chronicled by the words of the great poet Virgil, your husband Acerbas was murdered at the hands of your own brother, King Pygmalion of Tyre, who subsequently claimed the treasures of Acerbas that were now rightfully yours. Fearing the lengths from which your brother would pursue this vast wealth, you and your compatriots sailed for new lands. Arriving on the shores of North Africa, you tricked the local king with the simple manipulation of an ox hide, laying out a vast expanse of territory for your new home, the future kingdom of Carthage.",
"startIntroPart2": "Clever and inquisitive Dido, the world longs for a leader who can provide a shelter from the coming storm, guided by brilliant intuition and cunning. Can you lead the people in the creation of a new kingdom to rival that of once mighty Carthage? Can you build a civilization that will stand the test of time?",
"declaringWar": "Tell me, do you all know how numerous my armies, elephants and the gdadons are? No? Today, you shall find out!",
"attacked": "Fate is against you. You earned the animosity of Carthage in your exploration. Your days are numbered.",
"defeated": "The fates became to hate me. This is it? You wouldn't destroy us so without their help.",
"introduction": "The Phoenicians welcome you to this most pleasant kingdom. I am Dido, the queen of Carthage and all that belongs to it.",
"neutralHello": "It is done.",
"hateHello": "What is it now?",
"tradeRequest": "I had an idea and I realized I should tell it to you!",
"outerColor": [205,205,205],
"innerColor": [81,0,137],
"uniqueName": "Phoenician Heritage",
"uniques": ["Gain a free [Harbor] [in all coastal cities]","Land units may cross [Mountain] tiles after the first [Great General] is earned",
"Units ending their turn on [Mountain] tiles take [50] damage"],
"cities": ["Carthage","Utique","Hippo Regius","Gades","Saguntum","Carthago Nova","Panormus","Lilybaeum","Hadrumetum","Zama Regia",
"Karalis","Malaca","Leptis Magna","Hippo Diarrhytus","Motya","Sulci","Leptis Parva","Tharros","Soluntum","Lixus",
"Oea","Theveste","Ibossim","Thapsus","Aleria","Tingis","Abyla","Sabratha","Rusadir","Baecula",
"Saldae"]
},

View File

@ -86,7 +86,7 @@
"impassable": true,
"defenceBonus": 0.25,
"RGB": [120, 120, 120],
"uniques":["Rough terrain", "Has an elevation of [4] for visibility calculations", "Occurs in chains at high elevations"]
"uniques":["Rough terrain", "Has an elevation of [4] for visibility calculations", "Occurs in chains at high elevations", "Units ending their turn on this terrain take [50] damage"]
},
{
"name": "Snow",

View File

@ -161,6 +161,20 @@
"obsoleteTech": "Astronomy",
"attackSound": "nonmetalhit"
},
{
"name": "Quinquereme",
"unitType": "Melee Water",
"uniqueTo": "Carthage",
"replaces": "Trireme",
"movement": 4,
"strength": 13,
"cost": 45,
"requiredTech": "Sailing",
"uniques": ["Cannot enter ocean tiles"],
"upgradesTo": "Caravel",
"obsoleteTech": "Astronomy",
"attackSound": "nonmetalhit"
},
/*
{
"name": "Galley",
@ -339,6 +353,22 @@
"hurryCostModifier": 20,
"attackSound": "horse"
},
{
"name": "African Forest Elephant",
"unitType": "Mounted",
"uniqueTo": "Carthage",
"replaces": "Horseman",
"movement": 3,
"strength": 14,
"cost": 100,
"requiredTech": "Horseback Riding",
"upgradesTo": "Knight",
"obsoleteTech": "Metallurgy",
"promotions": ["Great Generals II"],
"uniques": ["Can move after attacking", "No defensive terrain bonus", "-[33]% Strength vs [City]","[-10]% Strength for enemy [Military] units in adjacent [All] tiles"],
"hurryCostModifier": 20,
"attackSound": "elephant"
},
{
"name": "Catapult",
"unitType": "Siege",

View File

@ -156,7 +156,8 @@ object SpecificUnitAutomation {
}
if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit
&& !unit.civInfo.isCityState()) return // ..unless you're a city state that was unable to settle its city on turn 1
&& !unit.civInfo.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1
&& unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting
val tilesNearCities = unit.civInfo.gameInfo.getCities().asSequence()
.flatMap {
@ -177,7 +178,7 @@ object SpecificUnitAutomation {
val possibleCityLocations = unit.getTile().getTilesInDistance(5)
.filter {
val tileOwner = it.getOwner()
it.isLand && (tileOwner == null || tileOwner == unit.civInfo) // don't allow settler to settle inside other civ's territory
it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civInfo) // don't allow settler to settle inside other civ's territory
&& (unit.currentTile == it || unit.movement.canMoveTo(it))
&& it !in tilesNearCities
}.toList()

View File

@ -19,7 +19,8 @@ object UnitAutomation {
&& (tile.getOwner() == null || !tile.getOwner()!!.isCityState())
&& tile.neighbors.any { it.position !in unit.civInfo.exploredTiles }
&& unit.movement.canReach(tile)
&& (!unit.civInfo.isCityState() || tile.neighbors.any { it.getOwner() == unit.civInfo }) // Don't want city-states exploring far outside their borders
&& (!unit.civInfo.isCityState() || tile.neighbors.any { it.getOwner() == unit.civInfo } // Don't want city-states exploring far outside their borders
&& unit.getDamageFromTerrain(tile) <= 0) // Don't take unnecessary damage
}
internal fun tryExplore(unit: MapUnit): Boolean {
@ -66,7 +67,8 @@ object UnitAutomation {
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
val reachableTilesMaxWalkingDistance = reachableTiles
.filter { it.value.totalDistance == unit.currentMovement }
.filter { it.value.totalDistance == unit.currentMovement
&& unit.getDamageFromTerrain(it.key) <= 0 } // Don't end turn on damaging terrain for no good reason
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.keys.random())
}
@ -87,6 +89,9 @@ object UnitAutomation {
if (unit.civInfo.isBarbarian())
throw IllegalStateException("Barbarians is not allowed here.")
// Might die next turn - move!
if (unit.health <= unit.getDamageFromTerrain() && tryHealUnit(unit)) return
if (unit.isCivilian()) {
if (tryRunAwayIfNeccessary(unit)) return
@ -209,7 +214,10 @@ object UnitAutomation {
.filter { it.isCityCenter() && it.getCity()!!.civInfo.isAtWarWith(unit.civInfo) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBombardmentRange).toHashSet()
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3)
.filter { unit.getDamageFromTerrain(it) > 0 }
val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
val viableTilesForHealing = unitDistanceToTiles.keys
@ -230,7 +238,7 @@ object UnitAutomation {
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
if (currentUnitTile != bestTileForHealing
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile) - unit.getDamageFromTerrain())
unit.movement.moveToTile(bestTileForHealing)
unit.fortifyIfCan()
@ -272,16 +280,18 @@ object UnitAutomation {
unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT).toList()
).filter {
// Ignore units that would 1-shot you if you attacked
// Ignore units that would 1-shot you if you attacked. Account for taking terrain damage after the fact.
BattleDamage.calculateDamageToAttacker(MapUnitCombatant(unit),
it.tileToAttackFrom,
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
Battle.getMapCombatantOfTile(it.tileToAttack)!!)
+ unit.getDamageFromTerrain(it.tileToAttackFrom) < unit.health
}
if (unit.baseUnit.isRanged())
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health == 1 }
val closestEnemy = closeEnemies.minByOrNull { it.tileToAttack.aerialDistanceTo(unit.getTile()) }
val closestEnemy = closeEnemies.filter { unit.getDamageFromTerrain(it.tileToAttackFrom) <= 0 } // Don't attack from a mountain
.minByOrNull { it.tileToAttack.aerialDistanceTo(unit.getTile()) }
if (closestEnemy != null) {
unit.movement.headTowards(closestEnemy.tileToAttackFrom)
@ -313,7 +323,8 @@ object UnitAutomation {
val reachableTileNearSiegedCity = siegedCities
.flatMap { it.getCenterTile().getTilesAtDistance(2) }
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canMoveTo(it) && unit.movement.canReach(it) }
.firstOrNull { unit.movement.canMoveTo(it) && unit.movement.canReach(it)
&& unit.getDamageFromTerrain(it) <= 0 } // Avoid ending up on damaging terrain
if (reachableTileNearSiegedCity != null) {
unit.movement.headTowards(reachableTileNearSiegedCity)
@ -359,6 +370,7 @@ object UnitAutomation {
.filter {
it.key.aerialDistanceTo(closestReachableEnemyCity) <=
unitRange && it.key !in tilesInBombardRange
&& unit.getDamageFromTerrain(it.key) <= 0 // Don't set up on a mountain
}
.minByOrNull { it.value.totalDistance }?.key
@ -377,7 +389,7 @@ object UnitAutomation {
// don't head straight to the city, try to head to landing grounds -
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
val tileToHeadTo = closestReachableEnemyCity.getTilesInDistanceRange(3..4)
.filter { it.isLand }
.filter { it.isLand && unit.getDamageFromTerrain(it) <= 0 } // Don't head for hurty terrain
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) }
@ -500,7 +512,7 @@ object UnitAutomation {
}
/** Returns whether the civilian spends its turn hiding and not moving */
fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean {
fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean {
// This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy
// Cheaper than determining which enemies could attack us next turn
//todo - stay when we're stacked with a good military unit???
@ -534,7 +546,7 @@ object UnitAutomation {
return
}
val tileFurthestFromEnemy = reachableTiles.keys
.filter { unit.movement.canMoveTo(it) }
.filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health }
.maxByOrNull { countDistanceToClosestEnemy(unit, it) }
?: return // can't move anywhere!
unit.movement.moveToTile(tileFurthestFromEnemy)

View File

@ -1,6 +1,7 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException
@ -97,6 +98,12 @@ class CivilizationInfo {
@Transient
private var cachedMilitaryMight = -1
@Transient
var passThroughImpassableUnlocked = false // Cached Boolean equal to passableImpassables.isNotEmpty()
@Transient
var nonStandardTerrainDamage = false
var playerType = PlayerType.AI
/** Used in online multiplayer for human players */
@ -149,6 +156,8 @@ class CivilizationInfo {
// default false once we no longer want legacy save-game compatibility
var hasEverOwnedOriginalCapital: Boolean? = null
val passableImpassables = HashSet<String>() // For Carthage-like uniques
// For Aggressor, Warmonger status
private var numMinorCivsAttacked = 0
@ -197,6 +206,7 @@ class CivilizationInfo {
toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
//
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
toReturn.passableImpassables.addAll(passableImpassables)
toReturn.numMinorCivsAttacked = numMinorCivsAttacked
return toReturn
}
@ -643,6 +653,11 @@ class CivilizationInfo {
cityInfo.civInfo = this // must be before the city's setTransients because it depends on the tilemap, that comes from the currentPlayerCivInfo
cityInfo.setTransients()
}
passThroughImpassableUnlocked = passableImpassables.isNotEmpty()
// Cache whether this civ gets nonstandard terrain damage for performance reasons.
nonStandardTerrainDamage = getMatchingUniques("Units ending their turn on [] tiles take [] damage")
.any { gameInfo.ruleSet.terrains[it.params[0]]!!.damagePerTurn != it.params[1].toInt() }
}
fun updateSightAndResources() {
@ -913,6 +928,13 @@ class CivilizationInfo {
}
placedUnit.setupAbilityUses()
}
for (unique in getMatchingUniques("Land units may cross [] tiles after the first [] is earned")) {
if (unit.matchesFilter(unique.params[1])) {
passThroughImpassableUnlocked = true // Update the cached Boolean
passableImpassables.add(unique.params[0]) // Add to list of passable impassables
}
}
return placedUnit
}

View File

@ -672,8 +672,8 @@ class MapUnit {
}
}
getCitadelDamage()
getTerrainDamage()
doCitadelDamage()
doTerrainDamage()
}
fun startTurn() {
@ -882,30 +882,38 @@ class MapUnit {
return damageFactor
}
private fun getTerrainDamage() {
// hard coded mountain damage for now
if (getTile().baseTerrain == Constants.mountain) {
val tileDamage = 50
health -= tileDamage
private fun doTerrainDamage() {
val tileDamage = getDamageFromTerrain()
health -= tileDamage
if (health <= 0) {
civInfo.addNotification(
"Our [$name] took [$tileDamage] tile damage and was destroyed",
currentTile.position,
name,
NotificationIcon.Death
)
destroy()
} else civInfo.addNotification(
"Our [$name] took [$tileDamage] tile damage",
if (health <= 0) {
civInfo.addNotification(
"Our [$name] took [$tileDamage] tile damage and was destroyed",
currentTile.position,
name
name,
NotificationIcon.Death
)
}
destroy()
} else if (tileDamage > 0) civInfo.addNotification(
"Our [$name] took [$tileDamage] tile damage",
currentTile.position,
name
)
}
private fun getCitadelDamage() {
fun getDamageFromTerrain(tile: TileInfo = currentTile): Int {
if (civInfo.nonStandardTerrainDamage) {
for (unique in getMatchingUniques("Units ending their turn on [] tiles take [] damage")) {
if (unique.params[0] in tile.getAllTerrains().map { it.name }) {
return unique.params[1].toInt() // Use the damage from the unique
}
}
}
// Otherwise fall back to the defined standard damage
return tile.getAllTerrains().sumBy { it.damagePerTurn }
}
private fun doCitadelDamage() {
// Check for Citadel damage - note: 'Damage does not stack with other Citadels'
val citadelTile = currentTile.neighbors
.firstOrNull {

View File

@ -167,7 +167,13 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
* Does not consider if the [destination] tile can actually be entered, use [canMoveTo] for that.
* Returns an empty list if there's no way to get to the destination.
*/
fun getShortestPath(destination: TileInfo): List<TileInfo> {
fun getShortestPath(destination: TileInfo, avoidDamagingTerrain: Boolean = false): List<TileInfo> {
// First try and find a path without damaging terrain
if (!avoidDamagingTerrain && unit.civInfo.passThroughImpassableUnlocked && unit.baseUnit.isLandUnit()) {
val damageFreePath = getShortestPath(destination, true)
if (damageFreePath.isNotEmpty()) return damageFreePath
}
val currentTile = unit.getTile()
if (currentTile.position == destination) return listOf(currentTile) // edge case that's needed, so that workers will know that they can reach their own tile. *sigh*
@ -187,6 +193,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
for (tileToCheck in tilesToCheck) {
val distanceToTilesThisTurn = getDistanceToTilesWithinTurn(tileToCheck.position, movementThisTurn)
for (reachableTile in distanceToTilesThisTurn.keys) {
// Avoid damaging terrain on first pass
if (avoidDamagingTerrain && unit.getDamageFromTerrain(reachableTile) > 0)
continue
if (reachableTile == destination)
distanceToDestination[tileToCheck] = distanceToTilesThisTurn[reachableTile]!!.totalDistance
else {
@ -546,7 +555,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
if (tile.isImpassible()) {
// special exception - ice tiles are technically impassible, but some units can move through them anyway
// helicopters can pass through impassable tiles like mountains
if (!(tile.terrainFeatures.contains(Constants.ice) && unit.canEnterIceTiles) && !unit.canPassThroughImpassableTiles)
if (!(tile.terrainFeatures.contains(Constants.ice) && unit.canEnterIceTiles) && !unit.canPassThroughImpassableTiles
// carthage-like uniques sometimes allow passage through impassible tiles
&& !(unit.civInfo.passThroughImpassableUnlocked && unit.civInfo.passableImpassables.contains(tile.getLastTerrain().name)))
return false
}
if (tile.isLand

View File

@ -170,7 +170,10 @@ class Ruleset {
if (buildingsFile.exists()) buildings += createHashmap(jsonParser.getFromJson(Array<Building>::class.java, buildingsFile))
val terrainsFile = folderHandle.child("Terrains.json")
if (terrainsFile.exists()) terrains += createHashmap(jsonParser.getFromJson(Array<Terrain>::class.java, terrainsFile))
if (terrainsFile.exists()) {
terrains += createHashmap(jsonParser.getFromJson(Array<Terrain>::class.java, terrainsFile))
for (terrain in terrains.values) terrain.setTransients()
}
val resourcesFile = folderHandle.child("TileResources.json")
if (resourcesFile.exists()) tileResources += createHashmap(jsonParser.getFromJson(Array<TileResource>::class.java, resourcesFile))

View File

@ -40,6 +40,9 @@ class Terrain : NamedStats(), ICivilopediaText, IHasUniques {
var defenceBonus:Float = 0f
var impassable = false
@Transient
var damagePerTurn = 0
override var civilopediaText = listOf<FormattedLine>()
fun isRough(): Boolean = uniques.contains("Rough terrain")
@ -141,4 +144,10 @@ class Terrain : NamedStats(), ICivilopediaText, IHasUniques {
return textList
}
fun setTransients() {
damagePerTurn = uniqueObjects.sumBy {
if (it.placeholderText == "Units ending their turn on this terrain take [] damage") it.params[0].toInt() else 0
}
}
}

View File

@ -161,7 +161,7 @@ object UnitActions {
* (no movement left, too close to another city).
*/
fun getFoundCityAction(unit: MapUnit, tile: TileInfo): UnitAction? {
if (!unit.hasUnique("Founds a new city") || tile.isWater) return null
if (!unit.hasUnique("Founds a new city") || tile.isWater || tile.isImpassible()) return null
if (unit.currentMovement <= 0 || tile.getTilesInDistance(3).any { it.isCityCenter() })
return UnitAction(UnitActionType.FoundCity, action = null)

View File

@ -42,6 +42,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Bow](https://thenounproject.com/search/?q=bow&i=101736) By Arthur Shlain for Bowman
* [Fishing Vessel](https://thenounproject.com/term/fishing-vessel/23815/) By Luis Prado for Work Boats
* [Greek Trireme](https://thenounproject.com/search/?q=ancient%20boat&i=1626303) By Zachary McCune for Trireme
* [Greek Trireme](https://thenounproject.com/search/?q=ancient%20boat&i=1626303) By Zachary McCune for Quinquereme
* [Chariot](https://thenounproject.com/search/?q=Chariot&i=1189930) By Andrew Doane for Chariot Archer
* [Elephant](https://thenounproject.com/Luis/uploads/?i=14048) By Luis Prado for War Elephant
* [Centaur](https://thenounproject.com/search/?q=horse+archer&i=1791296) by Michael Wohlwend for Horse Archer
@ -62,6 +63,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Roman Helmet](https://thenounproject.com/search/?q=legion&i=440134) By parkjisun for Legion
* [Horse](https://thenounproject.com/search/?q=Horse&i=1373793) By AFY Studio for Horseman
* [Horse Head](https://thenounproject.com/search/?q=Cavalry&i=374037) By Juan Pablo Bravo for Companion Cavalry
* [Elephant](https://thenounproject.com/term/elephant/1302749) By Angriawan Ditya Zulkarnain for African Forest Elephant
* [Judge](https://thenounproject.com/search/?q=judge&i=1076388) By Krisztián Mátyás for Courthouse
* [Petra](https://thenounproject.com/search/?q=petra&i=2855893) By Ranah Pixel Studio for Petra
@ -553,6 +555,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Lion](https://thenounproject.com/search/?q=lion&i=76154) by Nikki Rodriguez for The Netherlands
* [Three Crowns](https://thenounproject.com/search/?q=three+crowns&i=1155972) by Daniel Falk for Sweden
* [Flag of Austria](https://thenounproject.com/term/flag-of-austria/3292053/) by Olena Panasovska, UA for Austria
* [Elephant](https://thenounproject.com/term/elephant/564421/) by Hea Poh Lin for Carthage
## Promotions