diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 031bb38422..d5524b8663 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -5,6 +5,7 @@ object Constants { const val settler = "Settler" const val greatGeneral = "Great General" + const val impassable = "Impassable" const val ocean = "Ocean" const val coast = "Coast" const val mountain = "Mountain" diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index a36e73df6f..2f1bd45188 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -210,7 +210,8 @@ class CityConstructions { //region state changing functions fun setTransients(){ - builtBuildingObjects = ArrayList(builtBuildings.map { cityInfo.getRuleset().buildings[it]!! }) + builtBuildingObjects = ArrayList(builtBuildings.map { cityInfo.getRuleset().buildings[it] + ?: throw java.lang.Exception("Building $it is not found!")}) } fun addProductionPoints(productionToAdd: Int) { diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 76d94c11be..77bbe076cd 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -280,9 +280,9 @@ class CivilizationInfo { fun isAtWarWith(otherCiv:CivilizationInfo): Boolean { if (otherCiv.civName == civName) return false // never at war with itself if (otherCiv.isBarbarian() || isBarbarian()) return true - if (!diplomacy.containsKey(otherCiv.civName)) // not encountered yet - return false - return getDiplomacyManager(otherCiv).diplomaticStatus == DiplomaticStatus.War + val diplomacyManager = diplomacy[otherCiv.civName] + ?: return false // not encountered yet + return diplomacyManager.diplomaticStatus == DiplomaticStatus.War } fun isAtWar() = diplomacy.values.any { it.diplomaticStatus== DiplomaticStatus.War && !it.otherCiv().isDefeated() } @@ -338,7 +338,8 @@ class CivilizationInfo { * And if they civs on't yet know who they are then they don;t know if they're barbarians =\ * */ fun setNationTransient(){ - nation = gameInfo.ruleSet.nations[civName]!! + nation = gameInfo.ruleSet.nations[civName] + ?: throw java.lang.Exception("Nation $civName is not found!") } fun setTransients() { @@ -451,13 +452,13 @@ class CivilizationInfo { } fun canEnterTiles(otherCiv: CivilizationInfo): Boolean { - if(otherCiv==this) return true - if(nation.isBarbarian() && gameInfo.turns >= gameInfo.difficultyObject.turnBarbariansCanEnterPlayerTiles) return true - if(!diplomacy.containsKey(otherCiv.civName)) // not encountered yet - return false - if(isAtWarWith(otherCiv)) return true - if(getDiplomacyManager(otherCiv).hasOpenBorders) return true - return false + if (otherCiv==this) return true + if (otherCiv.isBarbarian()) return true + if (nation.isBarbarian() && gameInfo.turns >= gameInfo.difficultyObject.turnBarbariansCanEnterPlayerTiles) + return true + val diplomacyManager = diplomacy[otherCiv.civName] + ?: return false // not encountered yet + return (diplomacyManager.hasOpenBorders || diplomacyManager.diplomaticStatus == DiplomaticStatus.War) } fun addNotification(text: String, location: Vector2?, color: Color) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 7b98111c47..cbad7ee45f 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -39,6 +39,9 @@ class MapUnit { @Transient var doubleMovementInCoast = false @Transient var doubleMovementInForestAndJungle = false @Transient var doubleMovementInSnowTundraAndHills = false + @Transient var canEnterIceTiles = false + @Transient var cannotEnterOceanTiles = false + @Transient var cannotEnterOceanTilesUntilAstronomy = false lateinit var owner: String lateinit var name: String @@ -134,11 +137,14 @@ class MapUnit { uniques.addAll(promotions.promotions.map { currentTile.tileMap.gameInfo.ruleSet.unitPromotions[it]!!.effect }) tempUniques = uniques - if("Ignores terrain cost" in uniques) ignoresTerrainCost = true - if("Rough terrain penalty" in uniques) roughTerrainPenalty = true - if("Double movement in coast" in uniques) doubleMovementInCoast = true - if("Double movement rate through Forest and Jungle" in uniques) doubleMovementInForestAndJungle = true - if("Double movement in Snow, Tundra and Hills" in uniques) doubleMovementInSnowTundraAndHills = true + ignoresTerrainCost = ("Ignores terrain cost" in uniques) + roughTerrainPenalty = ("Rough terrain penalty" in uniques) + doubleMovementInCoast = ("Double movement in coast" in uniques) + doubleMovementInForestAndJungle = ("Double movement rate through Forest and Jungle" in uniques) + doubleMovementInSnowTundraAndHills = ("Double movement in Snow, Tundra and Hills" in uniques) + canEnterIceTiles = ("Can enter ice tiles" in uniques) + cannotEnterOceanTiles = ("Cannot enter ocean tiles" in uniques) + cannotEnterOceanTilesUntilAstronomy = ("Cannot enter ocean tiles until Astronomy" in uniques) } fun hasUnique(unique:String): Boolean { @@ -307,7 +313,8 @@ class MapUnit { fun setTransients(ruleset: Ruleset) { promotions.unit=this mapUnitAction?.unit = this - baseUnit=ruleset.units[name]!! + baseUnit=ruleset.units[name] + ?: throw java.lang.Exception("Unit $name is not found!") updateUniques() } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 16c065bc83..d95ad3fd8a 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -363,7 +363,7 @@ open class TileInfo { if(!defencePercentString.startsWith("-")) defencePercentString = "+$defencePercentString" lineList += "[$defencePercentString] to unit defence".tr() } - if(getBaseTerrain().impassable) lineList += "Impassable".tr() + if(getBaseTerrain().impassable) lineList += Constants.impassable.tr() return lineList.joinToString("\n") } diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 27316627c1..4caae2447c 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -325,8 +325,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { && !(tile.isCityCenter() && tile.isCoastalTile())) return false - if (tile.terrainFeature == Constants.ice - && !unit.baseUnit.uniques.contains("Can enter ice tiles")) + if (tile.terrainFeature == Constants.ice && !unit.canEnterIceTiles) return false if (tile.isWater && unit.type.isLandUnit()) { @@ -335,8 +334,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { return false } if (tile.isOcean && unit.civInfo.nation.unique != UniqueAbility.WAYFINDING) { - if (unit.baseUnit.uniques.contains("Cannot enter ocean tiles")) return false - if (unit.baseUnit.uniques.contains("Cannot enter ocean tiles until Astronomy") + if (unit.cannotEnterOceanTiles) return false + if (unit.cannotEnterOceanTilesUntilAstronomy && !unit.civInfo.tech.isResearched("Astronomy")) return false } @@ -350,12 +349,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // AIs won't enter city-state's border. } - val unitsInTile = tile.getUnits() - if (unitsInTile.isNotEmpty()) { - val firstUnit = unitsInTile.first() - if (firstUnit.civInfo != unit.civInfo && unit.civInfo.isAtWarWith(firstUnit.civInfo)) - return false - } + val firstUnit = tile.getUnits().firstOrNull() + if (firstUnit != null && firstUnit.civInfo != unit.civInfo && unit.civInfo.isAtWarWith(firstUnit.civInfo)) + return false return true } diff --git a/core/src/com/unciv/models/ruleset/tile/Terrain.kt b/core/src/com/unciv/models/ruleset/tile/Terrain.kt index ad6f3b64bb..ebcad6828b 100644 --- a/core/src/com/unciv/models/ruleset/tile/Terrain.kt +++ b/core/src/com/unciv/models/ruleset/tile/Terrain.kt @@ -1,6 +1,7 @@ package com.unciv.models.ruleset.tile import com.badlogic.gdx.graphics.Color +import com.unciv.Constants import com.unciv.models.ruleset.Ruleset import com.unciv.models.stats.NamedStats import com.unciv.models.translations.tr @@ -23,7 +24,10 @@ class Terrain : NamedStats() { if(uniques.isNotEmpty()) sb.appendln(uniques.joinToString { it.tr() }) - sb.appendln("{Movement cost}: $movementCost".tr()) + if (impassable) + sb.appendln(Constants.impassable.tr()) + else + sb.appendln("{Movement cost}: $movementCost".tr()) if (defenceBonus != 0f) sb.appendln("{Defence bonus}: ".tr() + (defenceBonus * 100).toInt() + "%") diff --git a/core/src/com/unciv/ui/CivilopediaScreen.kt b/core/src/com/unciv/ui/CivilopediaScreen.kt index e7d66f1ea3..6af5931df6 100644 --- a/core/src/com/unciv/ui/CivilopediaScreen.kt +++ b/core/src/com/unciv/ui/CivilopediaScreen.kt @@ -3,9 +3,15 @@ package com.unciv.ui import com.unciv.ui.utils.AutoScrollPane as ScrollPane import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.* +import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.map.TileInfo import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.tile.Terrain +import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.translations.tr +import com.unciv.ui.tilegroups.TileGroup +import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.* import java.util.* @@ -15,16 +21,20 @@ class CivilopediaScreen(ruleset: Ruleset) : CameraStageBaseScreen() { private val categoryToEntries = LinkedHashMap>() private val categoryToButtons = LinkedHashMap() - private val entrySelectTable = Table().apply { defaults().pad(5f) } + private val entrySelectTable = Table().apply { defaults().pad(6f) } val description = "".toLabel() + fun select(category: String) { entrySelectTable.clear() for (entry in categoryToEntries[category]!! .sortedBy { it.name.tr() }){ // Alphabetical order of localized names val entryButton = Button(skin) if(entry.image!=null) - entryButton.add(entry.image).size(50f).padRight(10f) + if (category=="Terrains") + entryButton.add(entry.image).padRight(24f) + else + entryButton.add(entry.image).size(50f).padRight(10f) entryButton.add(entry.name.toLabel()) entryButton.onClick { description.setText(entry.description) @@ -35,29 +45,8 @@ class CivilopediaScreen(ruleset: Ruleset) : CameraStageBaseScreen() { init { onBackButtonClicked { UncivGame.Current.setWorldScreen() } - val buttonTable = Table() - buttonTable.pad(15f) - buttonTable.defaults().pad(10f) - val buttonTableScroll = ScrollPane(buttonTable) - val goToGameButton = TextButton("Close".tr(), skin) - goToGameButton.onClick { - game.setWorldScreen() - dispose() - } - - val topTable = Table() - topTable.add(goToGameButton).pad(10f) - topTable.add(buttonTableScroll) - - val entryTable = Table() - val splitPane = SplitPane(topTable, entryTable, true, skin) - splitPane.splitAmount = 0.2f - splitPane.setFillParent(true) - - stage.addActor(splitPane) - - description.setWrap(true) + val tileSetStrings = TileSetStrings() categoryToEntries["Buildings"] = ruleset.buildings.values .map { CivilopediaEntry(it.name,it.getDescription(false, null,ruleset), @@ -66,7 +55,8 @@ class CivilopediaScreen(ruleset: Ruleset) : CameraStageBaseScreen() { .map { CivilopediaEntry(it.name,it.getDescription(ruleset), ImageGetter.getResourceImage(it.name,50f)) } categoryToEntries["Terrains"] = ruleset.terrains.values - .map { CivilopediaEntry(it.name,it.getDescription(ruleset)) } + .map { CivilopediaEntry(it.name,it.getDescription(ruleset), + terrainImage(it, ruleset, tileSetStrings) ) } categoryToEntries["Tile Improvements"] = ruleset.tileImprovements.values .map { CivilopediaEntry(it.name,it.getDescription(ruleset,false), ImageGetter.getImprovementIcon(it.name,50f)) } @@ -87,6 +77,10 @@ class CivilopediaScreen(ruleset: Ruleset) : CameraStageBaseScreen() { categoryToEntries["Tutorials"] = tutorialController.getCivilopediaTutorials() .map { CivilopediaEntry(it.key.replace("_"," "), it.value.joinToString("\n\n") { line -> line.tr() }) } + val buttonTable = Table() + buttonTable.pad(15f) + buttonTable.defaults().pad(10f) + for (category in categoryToEntries.keys) { val button = TextButton(category.tr(), skin) button.style = TextButton.TextButtonStyle(button.style) @@ -94,19 +88,74 @@ class CivilopediaScreen(ruleset: Ruleset) : CameraStageBaseScreen() { button.onClick { select(category) } buttonTable.add(button) } - select("Tutorials") - val sp = ScrollPane(entrySelectTable) - sp.setupOverscroll(5f, 1f, 200f) - entryTable.add(sp).width(Value.percentWidth(0.25f, entryTable)).height(Value.percentHeight(0.7f, entryTable)) + + buttonTable.pack() + buttonTable.width = stage.width + val buttonTableScroll = ScrollPane(buttonTable) + + val goToGameButton = TextButton("Close".tr(), skin) + goToGameButton.onClick { + game.setWorldScreen() + dispose() + } + + val topTable = Table() + topTable.add(goToGameButton).pad(10f) + topTable.add(buttonTableScroll) + topTable.pack() + //buttonTable.height = topTable.height + + val entryTable = Table() + val splitPane = SplitPane(topTable, entryTable, true, skin) + splitPane.splitAmount = topTable.prefHeight / stage.height + entryTable.height = stage.height - topTable.prefHeight + splitPane.setFillParent(true) + + stage.addActor(splitPane) + + description.setWrap(true) + + val entrySelectScroll = ScrollPane(entrySelectTable) + entrySelectScroll.setupOverscroll(5f, 1f, 200f) + entryTable.add(entrySelectScroll) + .width(Value.percentWidth(0.25f, entryTable)) + .fillY() .pad(Value.percentWidth(0.02f, entryTable)) entryTable.add(ScrollPane(description)).colspan(4) .width(Value.percentWidth(0.65f, entryTable)) - .height(Value.percentHeight(0.7f, entryTable)) + .fillY() .pad(Value.percentWidth(0.02f, entryTable)) // Simply changing these to x*width, y*height won't work - buttonTable.width = stage.width + select("Tutorials") } + private fun terrainImage (terrain: Terrain, ruleset: Ruleset, tileSetStrings: TileSetStrings ): Actor? { + val tileInfo = TileInfo() + tileInfo.ruleset = ruleset + when(terrain.type) { + TerrainType.NaturalWonder -> { + tileInfo.naturalWonder = terrain.name + tileInfo.baseTerrain = terrain.turnsInto ?: Constants.grassland + } + TerrainType.TerrainFeature -> { + tileInfo.terrainFeature = terrain.name + tileInfo.baseTerrain = terrain.occursOn?.last() ?: Constants.grassland + } + else -> + tileInfo.baseTerrain = terrain.name + } + tileInfo.setTransients() + val group = TileGroup(tileInfo, TileSetStrings()) + group.showEntireMap = true + group.forMapEditorIcon = true + group.update() + return group +// val wrapper = Table() +// wrapper.add(group).pad(24f) +// wrapper.pad(2f,24f,2f,24f) +// wrapper.debug = true +// return wrapper + } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt b/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt index aa47f5b5db..eeef480f83 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt @@ -12,6 +12,8 @@ import com.unciv.logic.map.RoadStatus import com.unciv.models.translations.tr import com.unciv.ui.saves.Gzip import com.unciv.ui.utils.Popup +import com.unciv.ui.utils.enable +import com.unciv.ui.utils.isEnabled import com.unciv.ui.utils.onClick import com.unciv.ui.worldscreen.mainmenu.DropBox import kotlin.concurrent.thread @@ -19,8 +21,10 @@ import kotlin.concurrent.thread class MapEditorMenuPopup(mapEditorScreen: MapEditorScreen): Popup(mapEditorScreen){ init{ val mapNameEditor = TextField(mapEditorScreen.mapName, skin) - mapNameEditor.addListener{ mapEditorScreen.mapName=mapNameEditor.text; true } - add(mapNameEditor).row() + add(mapNameEditor).fillX().row() + mapNameEditor.selectAll() + mapNameEditor.maxLength = 240 // A few under max for most filesystems + mapEditorScreen.stage.keyboardFocus = mapNameEditor val newMapButton = TextButton("New map".tr(),skin) newMapButton.onClick { @@ -51,10 +55,28 @@ class MapEditorMenuPopup(mapEditorScreen: MapEditorScreen): Popup(mapEditorScree saveMapButton.onClick { mapEditorScreen.tileMap.mapParameters.name=mapEditorScreen.mapName mapEditorScreen.tileMap.mapParameters.type=MapType.custom - MapSaver.saveMap(mapEditorScreen.mapName,mapEditorScreen.tileMap) - UncivGame.Current.setWorldScreen() + thread ( name = "SaveMap" ) { + try { + MapSaver.saveMap(mapEditorScreen.mapName, mapEditorScreen.tileMap) + UncivGame.Current.setWorldScreen() + } catch (ex: Exception) { + ex.printStackTrace() + Gdx.app.postRunnable { + val cantLoadGamePopup = Popup(mapEditorScreen) + cantLoadGamePopup.addGoodSizedLabel("It looks like your map can't be saved!").row() + cantLoadGamePopup.addCloseButton() + cantLoadGamePopup.open(force = true) + } + } + } } + saveMapButton.isEnabled = mapNameEditor.text.isNotEmpty() add(saveMapButton).row() + mapNameEditor.addListener { + mapEditorScreen.mapName = mapNameEditor.text + saveMapButton.isEnabled = mapNameEditor.text.isNotEmpty() + true + } val copyMapAsTextButton = TextButton("Copy to clipboard".tr(), skin) copyMapAsTextButton.onClick { diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index 14f1522f02..eede9a7460 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -18,7 +18,7 @@ import com.unciv.ui.utils.setFontSize class MapEditorScreen(): CameraStageBaseScreen() { val ruleset = RulesetCache.getBaseRuleset() - var mapName = "My first map" + var mapName = "" var tileMap = TileMap() lateinit var mapHolder: EditorMapHolder diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index 66a871c25a..34dcc256b9 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -42,11 +42,13 @@ class LoadGameScreen : PickerScreen() { if (ex is UncivShowableException && ex.localizedMessage != null) { // thrown exceptions are our own tests and can be shown to the user cantLoadGamePopup.addGoodSizedLabel(ex.localizedMessage).row() + cantLoadGamePopup.addCloseButton() cantLoadGamePopup.open() } else { cantLoadGamePopup.addGoodSizedLabel("If you could copy your game data (\"Copy saved game to clipboard\" - ").row() cantLoadGamePopup.addGoodSizedLabel(" paste into an email to yairm210@hotmail.com)").row() cantLoadGamePopup.addGoodSizedLabel("I could maybe help you figure out what went wrong, since this isn't supposed to happen!").row() + cantLoadGamePopup.addCloseButton() cantLoadGamePopup.open() ex.printStackTrace() } diff --git a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt index 803e4ae452..eb47635180 100644 --- a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt +++ b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt @@ -113,7 +113,10 @@ fun Button.enable() { color = Color.WHITE touchable = Touchable.enabled } - +var Button.isEnabled: Boolean + //Todo: Use in PromotionPickerScreen, TradeTable, WorldScreen.updateNextTurnButton + get() = touchable == Touchable.enabled + set(value) = if (value) enable() else disable() fun colorFromRGB(r: Int, g: Int, b: Int): Color { return Color(r/255f, g/255f, b/255f, 1f) diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index de1661ae7e..989ca8f44e 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -14,6 +14,7 @@ import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.* +import com.unciv.models.AttackableTile import com.unciv.models.UncivSound import com.unciv.models.ruleset.unit.UnitType import com.unciv.ui.map.TileGroupMap @@ -261,23 +262,24 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap if (UncivGame.Current.settings.singleTapMove || isAirUnit) 0.7f else 0.3f) } - val unitType = unit.type - val attackableTiles: List = if (unitType.isCivilian()) listOf() + val attackableTiles: List = if (unit.type.isCivilian()) listOf() else { - val tiles = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles()).map { it.tileToAttack } - tiles.filter { (UncivGame.Current.viewEntireMapForDebug || playerViewableTilePositions.contains(it.position)) } + BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles()) + .filter { (UncivGame.Current.viewEntireMapForDebug || + playerViewableTilePositions.contains(it.tileToAttack.position)) } + .distinctBy { it.tileToAttack } } for (attackableTile in attackableTiles) { - tileGroups[attackableTile]!!.showCircle(colorFromRGB(237, 41, 57)) - - val distance = unit.currentTile.aerialDistanceTo(attackableTile) - if (distance > unit.getRange()) - tileGroups[attackableTile]!!.showCrosshair(colorFromRGB(255, 75, 0)) - else - tileGroups[attackableTile]!!.showCrosshair(Color.RED) + tileGroups[attackableTile.tileToAttack]!!.showCircle(colorFromRGB(237, 41, 57)) + tileGroups[attackableTile.tileToAttack]!!.showCrosshair ( + // the targets which cannot be attacked without movements shown as orange-ish + if (attackableTile.tileToAttackFrom != unit.currentTile) + colorFromRGB(255, 75, 0) + else Color.RED + ) } // Fade out less relevant images if a military unit is selected diff --git a/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt b/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt new file mode 100644 index 0000000000..ecc29f5bf1 --- /dev/null +++ b/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt @@ -0,0 +1,238 @@ +// Taken from https://github.com/TomGrill/gdx-testing +package com.unciv.logic.map + +import com.unciv.Constants +import com.unciv.logic.city.CityInfo +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.diplomacy.DiplomacyManager +import com.unciv.logic.civilization.diplomacy.DiplomaticStatus +import com.unciv.models.ruleset.Nation +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.models.ruleset.unit.UnitType +import com.unciv.testing.GdxTestRunner +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(GdxTestRunner::class) +class UnitMovementAlgorithmsTests { + + private var tile = TileInfo() + private var civInfo = CivilizationInfo() + private var ruleSet = Ruleset() + private var unit = MapUnit() + + @Before + fun initTheWorld() { + RulesetCache.loadRulesets() + ruleSet = RulesetCache.getBaseRuleset() + tile.ruleset = ruleSet + civInfo.tech.techsResearched.addAll(ruleSet.technologies.keys) + civInfo.tech.embarkedUnitsCanEnterOcean = true + civInfo.tech.unitsCanEmbark = true + civInfo.nation = Nation().apply { + name = "My nation" + cities = arrayListOf("The Capital") + } + unit.civInfo = civInfo + } + + @Test + fun canPassThroughPassableTerrains() { + for (terrain in ruleSet.terrains.values) { + tile.baseTerrain = terrain.name + tile.setTransients() + + unit.baseUnit = BaseUnit().apply { unitType = UnitType.Melee } + + Assert.assertTrue(terrain.name, terrain.impassable != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun unitCanEnterTheCity() { + val map = TileMap() + tile.baseTerrain = Constants.hill + tile.tileMap = map + tile.setTransients() + + val otherTile = tile.clone() + otherTile.baseTerrain = Constants.coast + otherTile.position.y = 1f + + map.tileMatrix.add(arrayListOf(tile, otherTile)) + + val city = CityInfo() + city.location = tile.position + city.civInfo = civInfo + tile.owningCity = city + + for (type in UnitType.values()) + { + unit.owner = civInfo.civName + unit.baseUnit = BaseUnit().apply { unitType = type } + Assert.assertTrue(type.name, unit.movement.canPassThrough(tile)) + } + } + + @Test + fun waterUnitCanNOTEnterLand() { + for (terrain in ruleSet.terrains.values) { + if (terrain.impassable) continue + tile.baseTerrain = terrain.name + tile.setTransients() + + for (type in UnitType.values()) { + if (type == UnitType.City) continue + unit.baseUnit = BaseUnit().apply { unitType = type } + Assert.assertTrue("%s cannot be at %s".format(type, terrain.name), + (type.isWaterUnit() && tile.isLand) != unit.movement.canPassThrough(tile)) + } + } + } + + @Test + fun canNOTEnterIce() { + tile.baseTerrain = Constants.ocean + tile.terrainFeature = Constants.ice + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + if (type == UnitType.WaterSubmarine) { + unit.baseUnit.uniques.add("Can enter ice tiles") + } + unit.updateUniques() + + Assert.assertTrue("$type cannot be in Ice", + (type == UnitType.WaterSubmarine) == unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterNaturalWonder() { + tile.baseTerrain = Constants.plains + tile.naturalWonder = "Wonder Thunder" + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertFalse("$type must not enter Wonder tile", unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterCoastUntilProperTechIsResearched() { + + civInfo.tech.unitsCanEmbark = false + + tile.baseTerrain = Constants.coast + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertTrue("$type cannot be in Coast", + unit.type.isLandUnit() != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterOceanUntilProperTechIsResearched() { + + civInfo.tech.embarkedUnitsCanEnterOcean = false + + tile.baseTerrain = Constants.ocean + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertTrue("$type cannot be in Ocean", + unit.type.isLandUnit() != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterOceanWithLimitations() { + + tile.baseTerrain = Constants.ocean + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { + unitType = type + if (type == UnitType.Melee) + uniques.add("Cannot enter ocean tiles") + if (type == UnitType.Ranged) + uniques.add("Cannot enter ocean tiles until Astronomy") + } + unit.updateUniques() + + Assert.assertTrue("$type cannot be in Ocean", + (type == UnitType.Melee) != unit.movement.canPassThrough(tile)) + + civInfo.tech.techsResearched.remove("Astronomy") + + Assert.assertTrue("$type cannot be in Ocean until Astronomy", + (type == UnitType.Melee || + type == UnitType.Ranged) != unit.movement.canPassThrough(tile)) + + civInfo.tech.techsResearched.add("Astronomy") + } + } + + @Test + fun canNOTPassThroughTileWithEnemyUnits() { + tile.baseTerrain = Constants.grassland + tile.setTransients() + + val otherCiv = CivilizationInfo() + otherCiv.civName = "Barbarians" // they are always enemies + otherCiv.nation = Nation().apply { name = "Barbarians" } + val otherUnit = MapUnit() + otherUnit.civInfo = otherCiv + tile.militaryUnit = otherUnit + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertFalse("$type must not enter occupied tile", unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTPassForeignTiles() { + tile.baseTerrain = Constants.desert + tile.setTransients() + + val otherCiv = CivilizationInfo() + otherCiv.civName = "Other civ" + otherCiv.nation = Nation().apply { name = "Other nation" } + + val city = CityInfo() + city.location = tile.position.cpy().add(1f,1f) + city.civInfo = otherCiv + tile.owningCity = city + + unit.baseUnit = BaseUnit().apply { unitType = UnitType.Melee } + unit.owner = civInfo.civName + + Assert.assertFalse("Unit must not enter other civ tile", unit.movement.canPassThrough(tile)) + + city.location = tile.position + + Assert.assertFalse("Unit must not enter other civ city", unit.movement.canPassThrough(tile)) + + city.hasJustBeenConquered = true + civInfo.diplomacy["Other civ"] = DiplomacyManager(otherCiv, "Other civ") + civInfo.getDiplomacyManager(otherCiv).diplomaticStatus = DiplomaticStatus.War + + Assert.assertTrue("Unit can capture other civ city", unit.movement.canPassThrough(tile)) + } +} \ No newline at end of file