mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-10 15:59:33 +07:00
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:
BIN
android/Images/OtherIcons/Blockade.png
Normal file
BIN
android/Images/OtherIcons/Blockade.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
android/Images/TileIcons/Blockaded.png
Normal file
BIN
android/Images/TileIcons/Blockaded.png
Normal file
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 |
@ -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."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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 #######################
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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" +
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user