diff --git a/android/assets/jsons/Civ V - Vanilla/UnitTypes.json b/android/assets/jsons/Civ V - Vanilla/UnitTypes.json index ba5e371ded..31a3db20d1 100644 --- a/android/assets/jsons/Civ V - Vanilla/UnitTypes.json +++ b/android/assets/jsons/Civ V - Vanilla/UnitTypes.json @@ -50,7 +50,7 @@ { "name": "Submarine", "movementType": "Water", - "uniques": ["Can enter ice tiles", "Invisible to others"] + "uniques": ["Can enter ice tiles", "Invisible to non-adjacent units", "Can see invisible [Submarine] units"] }, { "name": "Aircraft Carrier", diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 0c7a337023..3b5580b3ce 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -1034,7 +1034,7 @@ "cost": 325, "requiredTech": "Refrigeration", "upgradesTo": "Nuclear Submarine", - "uniques": ["+[75]% Strength when attacking", "Can only attack [Water] tiles", "Can attack submarines"], + "uniques": ["+[75]% Strength when attacking", "Can only attack [Water] tiles"], "attackSound": "torpedo" }, { @@ -1173,7 +1173,7 @@ "interceptRange": 2, "cost": 375, "requiredTech": "Combustion", - "uniques": ["Can attack submarines", "[40]% chance to intercept air attacks", + "uniques": ["Can see invisible [Submarine] units", "[40]% chance to intercept air attacks", "May withdraw before melee ([80]%)", "+[100]% Strength vs [submarine units]"], "attackSound": "shipguns" }, @@ -1391,8 +1391,7 @@ "rangedStrength": 85, "cost": 425, "requiredTech": "Telecommunications", - "uniques": ["+[75]% Strength when attacking", "Can only attack [Water] tiles", "Can attack submarines", - "[+1] Visibility Range", "Can carry [2] [Missile] units"], + "uniques": ["+[75]% Strength when attacking", "Can only attack [Water] tiles", "[+1] Visibility Range", "Can carry [2] [Missile] units"], "attackSound": "torpedo" }, { @@ -1414,7 +1413,7 @@ "interceptRange": 2, "cost": 425, "requiredTech": "Robotics", - "uniques": ["[100]% chance to intercept air attacks", "Can attack submarines", + "uniques": ["[100]% chance to intercept air attacks", "Can see invisible [Submarine] units", "Ranged attacks may be performed over obstacles", "Can carry [3] [Missile] units", "+[100]% Strength vs [submarine units]"], "attackSound": "shipguns" diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 7561037379..9e7f92966c 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1169,4 +1169,5 @@ in all cities with a garrison = Only available after [] turns = This Unit upgrades for free = [stats] when a city adopts this religion for the first time = -Never destroyed when the city is captured = \ No newline at end of file +Never destroyed when the city is captured = +Invisible to others = \ No newline at end of file diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index cba1378ea6..be41f373c2 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -169,7 +169,7 @@ class GameInfo { it.militaryUnit != null && it.militaryUnit!!.civInfo != thisPlayer && thisPlayer.isAtWarWith(it.militaryUnit!!.civInfo) && (it.getOwner() == thisPlayer || it.neighbors.any { neighbor -> neighbor.getOwner() == thisPlayer } - && (!it.militaryUnit!!.isInvisible() || viewableInvisibleTiles.contains(it.position))) + && (!it.militaryUnit!!.isInvisible(thisPlayer) || viewableInvisibleTiles.contains(it.position))) } // enemy units ON our territory diff --git a/core/src/com/unciv/logic/automation/BattleHelper.kt b/core/src/com/unciv/logic/automation/BattleHelper.kt index f7b1edba86..d20a2d0f6a 100644 --- a/core/src/com/unciv/logic/automation/BattleHelper.kt +++ b/core/src/com/unciv/logic/automation/BattleHelper.kt @@ -95,15 +95,11 @@ object BattleHelper { ) return false - //only submarine and destroyer can attack submarine - //garrisoned submarine can be attacked by anyone, or the city will be in invincible - if (tileCombatant.isInvisible() && !tile.isCityCenter()) { - if (combatant is MapUnitCombatant - && combatant.unit.hasUnique("Can attack submarines") - && combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)) { - return true - } - return false + // Only units with the right unique can view submarines (or other invisible units) from more then one tile away. + // Garrisoned invisible units can be attacked by anyone, as else the city will be in invincible. + if (tileCombatant.isInvisible(combatant.getCivInfo()) && !tile.isCityCenter()) { + return combatant is MapUnitCombatant + && combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position) } return true } diff --git a/core/src/com/unciv/logic/battle/CityCombatant.kt b/core/src/com/unciv/logic/battle/CityCombatant.kt index 4b66008f29..996e02e012 100644 --- a/core/src/com/unciv/logic/battle/CityCombatant.kt +++ b/core/src/com/unciv/logic/battle/CityCombatant.kt @@ -18,7 +18,7 @@ class CityCombatant(val city: CityInfo) : ICombatant { override fun getTile(): TileInfo = city.getCenterTile() override fun getName(): String = city.name override fun isDefeated(): Boolean = city.health == 1 - override fun isInvisible(): Boolean = false + override fun isInvisible(to: CivilizationInfo): Boolean = false override fun canAttack(): Boolean = city.canBombard() override fun matchesCategory(category: String) = category == "City" || category == "All" override fun getAttackSound() = UncivSound.Bombard diff --git a/core/src/com/unciv/logic/battle/ICombatant.kt b/core/src/com/unciv/logic/battle/ICombatant.kt index afe1554506..2b924f386d 100644 --- a/core/src/com/unciv/logic/battle/ICombatant.kt +++ b/core/src/com/unciv/logic/battle/ICombatant.kt @@ -5,7 +5,7 @@ import com.unciv.logic.map.TileInfo import com.unciv.models.UncivSound import com.unciv.models.ruleset.unit.UnitType -interface ICombatant{ +interface ICombatant { fun getName(): String fun getHealth():Int fun getMaxHealth():Int @@ -16,7 +16,7 @@ interface ICombatant{ fun isDefeated():Boolean fun getCivInfo(): CivilizationInfo fun getTile(): TileInfo - fun isInvisible(): Boolean + fun isInvisible(to: CivilizationInfo): Boolean fun canAttack(): Boolean fun matchesCategory(category:String): Boolean fun getAttackSound(): UncivSound diff --git a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt index 22d438d42b..1a9455887c 100644 --- a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt +++ b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt @@ -13,7 +13,7 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant { override fun getTile(): TileInfo = unit.getTile() override fun getName(): String = unit.name override fun isDefeated(): Boolean = unit.health <= 0 - override fun isInvisible(): Boolean = unit.isInvisible() + override fun isInvisible(to: CivilizationInfo): Boolean = unit.isInvisible(to) override fun canAttack(): Boolean = unit.canAttack() override fun matchesCategory(category:String) = unit.matchesFilter(category) override fun getAttackSound() = unit.baseUnit.attackSound.let { @@ -36,7 +36,7 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant { } override fun getUnitType(): UnitType { - return unit.type!! + return unit.type } override fun toString(): String { diff --git a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt index 2746d61256..74940e0322 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt @@ -12,12 +12,28 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) { val newViewableInvisibleTiles = HashSet() newViewableInvisibleTiles.addAll(civInfo.getCivUnits() - .filter { it.hasUnique("Can attack submarines") } - .flatMap { it.viewableTiles.asSequence() }) + // "Can attack submarines" unique deprecated since 3.16.9 + .filter { attacker -> attacker.hasUnique("Can see invisible [] units") || attacker.hasUnique("Can attack submarines") } + .flatMap { attacker -> + attacker.viewableTiles + .asSequence() + .filter { tile -> + ( tile.militaryUnit != null + && attacker.getMatchingUniques("Can see invisible [] units") + .any { unique -> tile.militaryUnit!!.matchesFilter(unique.params[0]) } + ) || ( + tile.militaryUnit != null + // "Can attack submarines" unique deprecated since 3.16.9 + && attacker.hasUnique("Can attack submarines") + && tile.militaryUnit!!.matchesFilter("Submarine") + ) + } + } + ) civInfo.viewableInvisibleUnitsTiles = newViewableInvisibleTiles - // updating the viewable tiles also affects the explored tiles, obvs + // updating the viewable tiles also affects the explored tiles, obviously. // So why don't we play switcharoo with the explored tiles as well? // Well, because it gets REALLY LARGE so it's a lot of memory space, // and we never actually iterate on the explored tiles (only check contains()), diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 8428dc9198..c6b099eee9 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -354,9 +354,13 @@ class MapUnit { return currentTile.isWater } - fun isInvisible(): Boolean { + fun isInvisible(to: CivilizationInfo): Boolean { if (hasUnique("Invisible to others")) return true + if (hasUnique("Invisible to non-adjacent units")) + return getTile().getTilesInDistance(1).none { + it.getOwner() == to || it.getUnits().any { unit -> unit.owner == to.civName } + } return false } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index d67319bb2a..6aec5a9c53 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -637,7 +637,7 @@ open class TileInfo { val unitsInTile = getUnits() if (unitsInTile.none()) return false if (unitsInTile.first().civInfo != viewingCiv && - unitsInTile.firstOrNull { it.isInvisible() } != null) { + unitsInTile.firstOrNull { it.isInvisible(viewingCiv) } != null) { return true } return false diff --git a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt index 0b09ed8d6a..4c57b85d72 100644 --- a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt +++ b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt @@ -74,20 +74,20 @@ class BattleTable(val worldScreen: WorldScreen): Table() { val attackerCiv = worldScreen.viewingCiv val defender: ICombatant? = Battle.getMapCombatantOfTile(selectedTile) - if(defender==null || - !includeFriendly && defender.getCivInfo()==attackerCiv ) + if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv)) return null // no enemy combatant in tile - val canSeeDefender = if(UncivGame.Current.viewEntireMapForDebug) true - else { - when { - defender.isInvisible() -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile) - defender.isCity() -> attackerCiv.exploredTiles.contains(selectedTile.position) - else -> attackerCiv.viewableTiles.contains(selectedTile) + val canSeeDefender = + if (UncivGame.Current.viewEntireMapForDebug) true + else { + when { + defender.isInvisible(attackerCiv) -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile) + defender.isCity() -> attackerCiv.exploredTiles.contains(selectedTile.position) + else -> attackerCiv.viewableTiles.contains(selectedTile) + } } - } - if(!canSeeDefender) return null + if (!canSeeDefender) return null return defender } @@ -97,14 +97,14 @@ class BattleTable(val worldScreen: WorldScreen): Table() { val attackerNameWrapper = Table() val attackerLabel = attacker.getName().toLabel() - if(attacker is MapUnitCombatant) + if (attacker is MapUnitCombatant) attackerNameWrapper.add(UnitGroup(attacker.unit,25f)).padRight(5f) attackerNameWrapper.add(attackerLabel) add(attackerNameWrapper) val defenderNameWrapper = Table() val defenderLabel = Label(defender.getName().tr(), skin) - if(defender is MapUnitCombatant) + if (defender is MapUnitCombatant) defenderNameWrapper.add(UnitGroup(defender.unit,25f)).padRight(5f) defenderNameWrapper.add(defenderLabel) add(defenderNameWrapper).row()