Blockade mechanics (#8654)

* Fix centering of unit HP bar

* Blockade mechanics: land tiles

* Blockade mechanics: water tiles

* Don't blockade water tile if it has friendly military unit

* Blockade mechanics: cities

* Added tile/city blockade tutorials. Added city blockade status icon.

---------

Co-authored-by: vegeta1k95 <vfylfhby>
This commit is contained in:
vegeta1k95
2023-02-14 21:13:25 +01:00
committed by GitHub
parent d9b259c7fc
commit a8a458ab48
16 changed files with 340 additions and 214 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 501 KiB

View File

@ -377,5 +377,18 @@
"If the Interceptor is not an Air Unit (eg Land or Sea), the Air Sweeping unit and Interceptor take no damage!",
"If the Interceptor is an Air Unit, the two units will damage each other in a straight fight with no Interception bonuses. And only the Attacking Air Sweep Unit gets any Air Sweep strength bonuses."
]
},
{
"name": "City Tile Blockade",
"steps": [
"One of your tiles is blocked by an enemy: when an enemy unit stands on a tile you own, the tile will not produce yields and cannot be worked by a city this turn. City will reallocate population from a blocked tile automatically.",
"Enemy military land units block tiles they are standing on. Enemy military naval units additionally block adjacent water tiles. To protect your tiles from blockade, place a friendly military unit on it or fight off invaders."
]
},
{
"name": "City Blockade",
"steps": [
"One of your cities is under a naval blockade! When all adjacent water tiles of a coastal city are blocked - city loses harbor connection to all other cities, including capital. Make sure to de-blockade cities by deploying friendly military naval units to fight off invaders."
]
}
]

View File

@ -11020,6 +11020,16 @@ If the Interceptor is not an Air Unit (eg Land or Sea), the Air Sweeping unit an
# Requires translation!
If the Interceptor is an Air Unit, the two units will damage each other in a straight fight with no Interception bonuses. And only the Attacking Air Sweep Unit gets any Air Sweep strength bonuses. =
# Requires translation!
City Tile Blockade =
# Requires translation!
One of your tiles is blocked by an enemy: when an enemy unit stands on a tile you own, the tile will not produce yields and cannot be worked by a city this turn. City will reallocate population from a blocked tile automatically. =
# Requires translation!
Enemy military land units block tiles they are standing on. Enemy military naval units additionally block adjacent water tiles. To protect your tiles from blockade, place a friendly military unit on it or fight off invaders. =
# Requires translation!
City Blockade =
# Requires translation!
One of your cities is under a naval blockade! When all adjacent water tiles of a coastal city are blocked - city loses harbor connection to all other cities, including capital. Make sure to de-blockade cities by deploying friendly military naval units to fight off invaders. =
#################### Lines from Unique Types #######################

View File

@ -84,7 +84,7 @@ class City : IsPartOfGameInfoSerialization {
var attackedThisTurn = false
var hasSoldBuildingThisTurn = false
var isPuppet = false
var updateCitizens = false // flag so that on endTurn() the Governor reassigns Citizens
var updateCitizens = false // flag so that on startTurn() the Governor reassigns Citizens
var cityAIFocus: CityFocus = CityFocus.NoFocus
var avoidGrowth: Boolean = false
@Transient var currentGPPBonus: Int = 0 // temporary variable saved for rankSpecialist()
@ -158,7 +158,27 @@ class City : IsPartOfGameInfoSerialization {
fun isWeLoveTheKingDayActive() = hasFlag(CityFlags.WeLoveTheKing)
fun isInResistance() = hasFlag(CityFlags.Resistance)
fun isBlockaded(): Boolean {
// Landlocked cities are not blockaded
if (!isCoastal())
return false
// Coastal cities are blocked if every adjacent water tile is blocked
for (tile in getCenterTile().neighbors) {
// Consider only water tiles
if (!tile.isWater)
continue
// One unblocked tile breaks whole city blockade
if (!tile.isBlockaded())
return false
}
// All tiles are blocked
return true
}
fun getRuleset() = civ.gameInfo.ruleset

View File

@ -354,7 +354,7 @@ class CityStats(val city: City) {
fun updateTileStats() {
val stats = Stats()
val localUniqueCache = LocalUniqueCache()
val workedTiles = city.tilesInRange
val workedTiles = city.tilesInRange.asSequence()
.filter {
city.location == it.position
|| city.isWorked(it)
@ -362,9 +362,15 @@ class CityStats(val city: City) {
?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true
|| it.terrainHasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
}
for (cell in workedTiles) {
val cellStats = cell.stats.getTileStats(city, city.civ, localUniqueCache)
stats.add(cellStats)
for (tile in workedTiles) {
if (tile.isBlockaded() && city.isWorked(tile)) {
city.workedTiles.remove(tile.position)
city.lockedTiles.remove(tile.position)
city.updateCitizens = true
continue
}
val tileStats = tile.stats.getTileStats(city, city.civ, localUniqueCache)
stats.add(tileStats)
}
statsFromTiles = stats
}

View File

@ -150,7 +150,7 @@ class CityPopulationManager : IsPartOfGameInfoSerialization {
val currentCiv = city.civ
val tilesToEvaluate = city.getCenterTile().getTilesInDistance(3)
.filter { it.getOwner() == currentCiv }.toList().asSequence()
.filter { it.getOwner() == currentCiv && !it.isBlockaded() }.toList().asSequence()
for (i in 1..getFreePopulation()) {
//evaluate tiles
val (bestTile, valueBestTile) = tilesToEvaluate

View File

@ -81,7 +81,7 @@ class CapitalConnectionsFinder(private val civInfo: Civilization) {
transportType = if(cityToConnectFrom.wasPreviouslyReached(railroad,null)) harborFromRailroad else harborFromRoad,
overridingTransportType = harborFromRailroad,
tileFilter = { tile -> tile.isWater },
cityFilter = { city -> city.containsHarbor() && city.civ == civInfo } // use only own harbors
cityFilter = { city -> city.civ == civInfo && city.containsHarbor() && !city.isBlockaded() } // use only own harbors
)
}

View File

@ -429,6 +429,40 @@ open class Tile : IsPartOfGameInfoSerialization {
return civInfo.cities.firstOrNull { it.isWorked(this) }
}
fun isBlockaded(): Boolean {
val owner = getOwner() ?: return false
val unit = militaryUnit
// If tile has unit
if (unit != null) {
return when {
unit.civ == owner -> false // Own - unblocks tile;
unit.civ.isAtWarWith(owner) -> true // Enemy - blocks tile;
else -> false // Neutral - unblocks tile;
}
}
// No unit -> land tile is not blocked
if (isLand)
return false
// For water tiles need also to check neighbors:
// enemy military naval units blockade all adjacent water tiles.
for (neighbor in neighbors) {
// Check only water neighbors
if (!neighbor.isWater)
continue
val neighborUnit = neighbor.militaryUnit ?: continue
// Embarked units do not blockade adjacent tiles
if (neighborUnit.civ.isAtWarWith(owner) && !neighborUnit.isEmbarked())
return true
}
return false
}
fun isWorked(): Boolean = getWorkingCity() != null
fun providesYield() = getCity() != null && (isCityCenter() || isWorked()
|| getUnpillagedTileImprovement()?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true

View File

@ -50,4 +50,6 @@ enum class TutorialTrigger(val value: String, val isCivilopedia: Boolean = !valu
Inquisitors("Inquisitors"),
MayanCalendar("Maya_Long_Count_calendar_cycle"),
WeLoveTheKingDay("We_Love_The_King_Day"),
CityTileBlockade("City_Tile_Blockade"),
CityBlockade("City_Blockade")
}

View File

@ -10,6 +10,7 @@ import com.unciv.logic.automation.Automation
import com.unciv.logic.city.City
import com.unciv.logic.city.IConstruction
import com.unciv.logic.map.tile.Tile
import com.unciv.models.TutorialTrigger
import com.unciv.models.UncivSound
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.tile.TileImprovement
@ -24,6 +25,7 @@ import com.unciv.ui.popup.ConfirmPopup
import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.tilegroups.CityTileGroup
import com.unciv.ui.tilegroups.CityTileState
import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.KeyCharAndCode
@ -228,6 +230,10 @@ class CityScreen(
for (tileGroup in tileGroups) {
tileGroup.update()
tileGroup.layerMisc.removeHexOutline()
if (tileGroup.tileState == CityTileState.BLOCKADED)
displayTutorial(TutorialTrigger.CityTileBlockade)
when {
tileGroup.tile == nextTileToOwn ->
tileGroup.layerMisc.addHexOutline(colorFromRGB(200, 20, 220))
@ -335,7 +341,7 @@ class CityScreen(
val tile = tileGroup.tile
// Cycling as: Not-worked -> Worked -> Locked -> Not-worked
if (tileGroup.isWorkable) {
if (tileGroup.tileState == CityTileState.WORKABLE) {
if (!tile.providesYield() && city.population.getFreePopulation() > 0) {
city.workedTiles.add(tile.position)
game.settings.addCompletedTutorialTask("Reassign worked tiles")
@ -348,7 +354,7 @@ class CityScreen(
city.cityStats.update()
update()
} else if (tileGroup.isPurchasable) {
} else if (tileGroup.tileState == CityTileState.PURCHASABLE) {
val price = city.expansion.getGoldCostOfTile(tile)
val purchasePrompt = "Currently you have [${city.civ.gold}] [Gold].".tr() + "\n\n" +

View File

@ -15,6 +15,7 @@ import com.unciv.logic.city.City
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.city.PerpetualConstruction
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.TutorialTrigger
import com.unciv.models.translations.tr
import com.unciv.ui.cityscreen.CityReligionInfoTable
import com.unciv.ui.cityscreen.CityScreen
@ -31,6 +32,7 @@ import com.unciv.ui.utils.extensions.darken
import com.unciv.ui.utils.extensions.onClick
import com.unciv.ui.utils.extensions.toGroup
import com.unciv.ui.utils.extensions.toLabel
import com.unciv.ui.worldscreen.WorldScreen
import kotlin.math.max
import kotlin.math.min
@ -158,16 +160,22 @@ class AirUnitTable(city: City, numberOfUnits: Int, size: Float=14f) : BorderedTa
}
private class StatusTable(city: City, iconSize: Float = 18f) : Table() {
private class StatusTable(worldScreen: WorldScreen, city: City, iconSize: Float = 18f) : Table() {
init {
val padBetween = 2f
val viewingCiv = UncivGame.Current.worldScreen!!.viewingCiv
if (city.civ == viewingCiv && city.isConnectedToCapital() && !city.isCapital()) {
val connectionImage = ImageGetter.getStatIcon("CityConnection")
add(connectionImage).size(iconSize)
if (city.civ == viewingCiv) {
if (city.isBlockaded()) {
val connectionImage = ImageGetter.getImage("OtherIcons/Blockade")
add(connectionImage).size(iconSize)
worldScreen.displayTutorial(TutorialTrigger.CityBlockade)
} else if (!city.isCapital() && city.isConnectedToCapital()) {
val connectionImage = ImageGetter.getStatIcon("CityConnection")
add(connectionImage).size(iconSize)
}
}
if (city.isInResistance()) {
@ -429,7 +437,7 @@ class CityButton(val city: City, private val tileGroup: TileGroup): Table(BaseSc
}
// Add statuses: connection, resistance, puppet, raze, WLTKD
add(StatusTable(city)).padTop(3f)
add(StatusTable(worldScreen, city)).padTop(3f)
pack()

View File

@ -16,10 +16,16 @@ import com.unciv.ui.utils.extensions.setFontColor
import com.unciv.ui.utils.extensions.toGroup
import com.unciv.ui.utils.extensions.toLabel
enum class CityTileState {
NONE,
WORKABLE,
PURCHASABLE,
BLOCKADED
}
class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings) : TileGroup(tile,tileSetStrings) {
var isWorkable = false
var isPurchasable = false
var tileState = CityTileState.NONE
init {
layerMisc.touchable = Touchable.childrenOnly
@ -28,8 +34,7 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
override fun update(viewingCiv: Civilization?) {
super.update(city.civ)
isWorkable = false
isPurchasable = false
tileState = CityTileState.NONE
layerMisc.removeWorkedIcon()
var icon: Actor? = null
@ -57,7 +62,7 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
image.color = Color.WHITE.darken(0.5f)
label.setFontColor(Color.RED)
} else {
isPurchasable = true
tileState = CityTileState.PURCHASABLE
}
}
}
@ -85,24 +90,31 @@ class CityTileGroup(val city: City, tile: Tile, tileSetStrings: TileSetStrings)
// Do nothing
}
// Blockaded
tile.isBlockaded() -> {
icon = ImageGetter.getImage("TileIcons/Blockaded")
tileState = CityTileState.BLOCKADED
layerMisc.dimYields(true)
}
// Locked
tile.isLocked() -> {
icon = ImageGetter.getImage("TileIcons/Locked")
isWorkable = true
tileState = CityTileState.WORKABLE
layerMisc.dimYields(false)
}
// Worked
tile.isWorked() -> {
icon = ImageGetter.getImage("TileIcons/Worked")
isWorkable = true
tileState = CityTileState.WORKABLE
layerMisc.dimYields(false)
}
// Not-worked
else -> {
icon = ImageGetter.getImage("TileIcons/NotWorked")
isWorkable = true
tileState = CityTileState.WORKABLE
layerMisc.dimYields(true)
}
}

View File

@ -769,6 +769,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [Nothing](https://www.flaticon.com/free-icon/nothing_5084125) created by Freepik for Nothing construction process
- Icon for Unique created by [vegeta1k95](https://github.com/vegeta1k95)
- [Transform] created by letstalkaboutdune
- [Swords](https://thenounproject.com/icon/swords-1580316/) created by Muhajir ila Robbi for Blockaded tile marker
### Main menu