From 9016385f300ca73623cbecf62cc358cda1bcea5b Mon Sep 17 00:00:00 2001 From: SimonCeder <63475501+SimonCeder@users.noreply.github.com> Date: Sun, 3 Oct 2021 10:45:02 +0200 Subject: [PATCH] Barbarian spawning and camp placements (#5354) * barbarian camp placement, spawn countdowns * separate file * raging barbarians * faster spawn when attacked * Barbarian AI * works on old saves * template.properties * fix percent * no improvements unique * fix test fail * reviews * reviews pt 2 --- .../jsons/Civ V - Vanilla/Difficulties.json | 8 + .../jsons/Civ V - Vanilla/Terrains.json | 4 +- .../jsons/translations/template.properties | 2 + core/src/com/unciv/logic/BarbarianManager.kt | 271 ++++++++++++++++++ core/src/com/unciv/logic/GameInfo.kt | 89 +----- .../logic/automation/BarbarianAutomation.kt | 4 +- .../unciv/logic/automation/BattleHelper.kt | 9 +- core/src/com/unciv/logic/battle/Battle.kt | 21 +- core/src/com/unciv/logic/map/TileInfo.kt | 4 + .../unciv/models/metadata/GameParameters.kt | 3 + .../com/unciv/models/ruleset/Difficulty.kt | 2 + .../ui/newgamescreen/GameOptionsTable.kt | 9 +- 12 files changed, 330 insertions(+), 96 deletions(-) create mode 100644 core/src/com/unciv/logic/BarbarianManager.kt diff --git a/android/assets/jsons/Civ V - Vanilla/Difficulties.json b/android/assets/jsons/Civ V - Vanilla/Difficulties.json index 1537097f34..f629cd13cf 100644 --- a/android/assets/jsons/Civ V - Vanilla/Difficulties.json +++ b/android/assets/jsons/Civ V - Vanilla/Difficulties.json @@ -11,6 +11,7 @@ "policyCostModifier": 0.5, "unhappinessModifier": 0.4, "barbarianBonus": 0.75, + "barbarianSpawnDelay": 8, "playerBonusStartingUnits": [], // Note that the units from Eras.json are added to this pool. This should only contain bonus starting units. "aiCityGrowthModifier": 1.6, // that is to say it'll take them 1.6 times as long to grow the city "aiUnitCostModifier": 1.75, @@ -39,6 +40,7 @@ "policyCostModifier": 0.67, "unhappinessModifier": 0.6, "barbarianBonus": 0.5, + "barbarianSpawnDelay": 5, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1.3, "aiUnitCostModifier": 1.3, @@ -67,6 +69,7 @@ "policyCostModifier": 0.85, "unhappinessModifier": 0.75, "barbarianBonus": 0.4, + "barbarianSpawnDelay": 3, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1.1, "aiUnitCostModifier": 1.1, @@ -95,6 +98,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.33, + "barbarianSpawnDelay": 0, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1, "aiUnitCostModifier": 1, @@ -123,6 +127,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.25, + "barbarianSpawnDelay": 0, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.9, "aiUnitCostModifier": 0.85, @@ -151,6 +156,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.2, + "barbarianSpawnDelay": 0, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.85, "aiUnitCostModifier": 0.8, @@ -179,6 +185,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.1, + "barbarianSpawnDelay": 0, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.75, "aiUnitCostModifier": 0.65, @@ -207,6 +214,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0, + "barbarianSpawnDelay": 0, "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.6, "aiUnitCostModifier": 0.5, diff --git a/android/assets/jsons/Civ V - Vanilla/Terrains.json b/android/assets/jsons/Civ V - Vanilla/Terrains.json index c21a2059c7..eab8a363dc 100644 --- a/android/assets/jsons/Civ V - Vanilla/Terrains.json +++ b/android/assets/jsons/Civ V - Vanilla/Terrains.json @@ -157,7 +157,7 @@ "gold": -3, "movementCost": 2, "unbuildable": true, - "defenceBonus": -0.15 + "defenceBonus": -0.15, }, { "name": "Oasis", @@ -168,7 +168,7 @@ "unbuildable": true, "defenceBonus": -0.1, "occursOn": ["Desert"], - "uniques": ["Fresh water", "Rare feature"] + "uniques": ["Fresh water", "Rare feature", "Only [All Road] improvements may be built on this tile"] }, { "name": "Flood plains", diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 4cedaa5e04..9a9e6ce8be 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -308,6 +308,7 @@ Archipelago = Number of City-States = One City Challenge = No Barbarians = +Raging Barbarians = No Ancient Ruins = No Natural Wonders = Victory Conditions = @@ -1037,6 +1038,7 @@ Building cost modifier = Policy cost modifier = Unhappiness modifier = Bonus vs. Barbarians = +Barbarian spawning delay = Bonus starting units = AI settings = diff --git a/core/src/com/unciv/logic/BarbarianManager.kt b/core/src/com/unciv/logic/BarbarianManager.kt new file mode 100644 index 0000000000..68be5a7998 --- /dev/null +++ b/core/src/com/unciv/logic/BarbarianManager.kt @@ -0,0 +1,271 @@ +package com.unciv.logic + +import com.badlogic.gdx.math.Vector2 +import com.unciv.Constants +import com.unciv.logic.civilization.NotificationIcon +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap +import com.unciv.models.metadata.GameSpeed +import java.util.* +import kotlin.collections.HashMap +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.system.measureNanoTime + +class BarbarianManager { + val camps = HashMap() + + @Transient + lateinit var gameInfo: GameInfo + + @Transient + lateinit var tileMap: TileMap + + fun clone(): BarbarianManager { + val toReturn = BarbarianManager() + for (camp in camps.values.map { it.clone() }) + toReturn.camps[camp.position] = camp + return toReturn + } + + fun setTransients(gameInfo: GameInfo) { + this.gameInfo = gameInfo + this.tileMap = gameInfo.tileMap + + // Add any preexisting camps as Encampment objects + for (tile in tileMap.values) { + if (tile.improvement == Constants.barbarianEncampment + && camps[tile.position] == null) { + val newCamp = Encampment() + newCamp.position = tile.position + camps[newCamp.position] = newCamp + } + } + + for (camp in camps.values) + camp.gameInfo = gameInfo + } + + fun updateEncampments() { + // Check if camps were destroyed + for (position in camps.keys.toList()) { + if (tileMap[position].improvement != Constants.barbarianEncampment) { + camps.remove(position) + } + } + + // Possibly place a new encampment + placeBarbarianEncampment() + + // Update all existing camps + for (camp in camps.values) { + camp.update() + } + } + + /** Called when an encampment was attacked, will speed up time to next spawn */ + fun campAttacked(position: Vector2) { + camps[position]?.wasAttacked() + } + + fun placeBarbarianEncampment() { + // Before we do the expensive stuff, do a roll to see if we will place a camp at all + if (gameInfo.turns > 1 && Random().nextBoolean()) + return + + // Barbarians will only spawn in places that no one can see + val allViewableTiles = gameInfo.civilizations.asSequence().filterNot { it.isBarbarian() || it.isSpectator() } + .flatMap { it.viewableTiles }.toHashSet() + val fogTiles = tileMap.values.filter { it.isLand && it !in allViewableTiles } + + val fogTilesPerCamp = (tileMap.values.size.toFloat().pow(0.4f)).toInt() // Approximately + + // Check if we have more room + var campsToAdd = (fogTiles.size / fogTilesPerCamp) - camps.size + + // First turn of the game add 1/3 of all possible camps + if (gameInfo.turns == 1) { + campsToAdd /= 3 + campsToAdd = max(campsToAdd, 1) // At least 1 on first turn + } else if (campsToAdd > 0) + campsToAdd = 1 + + if (campsToAdd <= 0) return + + // Camps can't spawn within 7 tiles of each other or within 4 tiles of major civ capitals + val tooCloseToCapitals = gameInfo.civilizations.filterNot { it.isBarbarian() || it.isSpectator() || it.cities.isEmpty() || it.isCityState() } + .flatMap { it.getCapital().getCenterTile().getTilesInDistance(4) }.toSet() + val tooCloseToCamps = camps + .flatMap { tileMap[it.key].getTilesInDistance(7) }.toSet() + + val viableTiles = fogTiles.filter { + !it.isImpassible() + && it.resource == null + && it.terrainFeatures.none { feature -> gameInfo.ruleSet.terrains[feature]!!.hasUnique("Only [] improvements may be built on this tile") } + && it.neighbors.any { neighbor -> neighbor.isLand } + && it !in tooCloseToCapitals + && it !in tooCloseToCamps + }.toMutableList() + + var tile: TileInfo? + var addedCamps = 0 + var biasCoast = Random().nextInt(6) == 0 + + // Add the camps + while (addedCamps < campsToAdd) { + if (viableTiles.isEmpty()) + break + + // If we're biasing for coast, get a coast tile if possible + if (biasCoast) { + tile = viableTiles.filter { it.isCoastalTile() }.randomOrNull() + if (tile == null) + tile = viableTiles.random() + } else + tile = viableTiles.random() + + tile.improvement = Constants.barbarianEncampment + val newCamp = Encampment() + newCamp.position = tile.position + newCamp.gameInfo = gameInfo + camps[newCamp.position] = newCamp + notifyCivsOfBarbarianEncampment(tile) + addedCamps++ + + // Still more camps to add? + if (addedCamps < campsToAdd) { + // Remove some newly non-viable tiles + viableTiles.removeAll( tile.getTilesInDistance(7) ) + // Reroll bias + biasCoast = Random().nextInt(6) == 0 + } + } + } + + /** + * [CivilizationInfo.addNotification][Add a notification] to every civilization that have + * adopted Honor policy and have explored the [tile] where the Barbarian Encampment has spawned. + */ + private fun notifyCivsOfBarbarianEncampment(tile: TileInfo) { + gameInfo.civilizations.filter { + it.hasUnique("Notified of new Barbarian encampments") + && it.exploredTiles.contains(tile.position) + } + .forEach { it.addNotification("A new barbarian encampment has spawned!", tile.position, NotificationIcon.War) } + } +} + +class Encampment { + var countdown = 0 + var spawnedUnits = -1 + lateinit var position: Vector2 + + @Transient + lateinit var gameInfo: GameInfo + + fun clone(): Encampment { + val toReturn = Encampment() + toReturn.position = position + toReturn.countdown = countdown + toReturn.spawnedUnits = spawnedUnits + return toReturn + } + + fun update() { + if (countdown > 0) // Not yet + countdown-- + else if (spawnBarbarian()) { // Countdown at 0, try to spawn a barbarian + // Successful + spawnedUnits++ + resetCountdown() + } + } + + fun wasAttacked() { + countdown /= 2 + } + + /** Attempts to spawn a Barbarian from this encampment. Returns true if a unit was spawned. */ + private fun spawnBarbarian(): Boolean { + val tile = gameInfo.tileMap[position] + + // Empty camp - spawn a defender + if (tile.militaryUnit == null) { + return spawnOnTile(tile) // Try spawning a unit on this tile, return false if unsuccessful + } + + // Don't spawn wandering barbs too early + if (gameInfo.turns < 10) + return false + + // Too many barbarians around already? + val barbarianCiv = gameInfo.getBarbarianCivilization() + if (tile.getTilesInDistance(4).count { it.militaryUnit?.civInfo == barbarianCiv } > 2) + return false + + val canSpawnBoats = gameInfo.turns > 30 + val validTiles = tile.neighbors.toList().filterNot { + it.isImpassible() + || it.isCityCenter() + || it.getFirstUnit() != null + || (it.isWater && !canSpawnBoats) + || (it.hasUnique("Fresh water") && it.isWater) // No Lakes + } + if (validTiles.isEmpty()) return false + + return spawnOnTile(validTiles.random()) // Attempt to spawn a barbarian on a valid tile + } + + /** Attempts to spawn a barbarian on [tile], returns true if successful and false if unsuccessful. */ + private fun spawnOnTile(tile: TileInfo): Boolean { + val unitToSpawn = chooseBarbarianUnit(tile.isWater) ?: return false // return false if we didn't find a unit + val spawnedUnit = gameInfo.tileMap.placeUnitNearTile(tile.position, unitToSpawn, gameInfo.getBarbarianCivilization()) + return (spawnedUnit != null) + } + + private fun chooseBarbarianUnit(naval: Boolean): String? { + // if we don't make this into a separate list then the retain() will happen on the Tech keys, + // which effectively removes those techs from the game and causes all sorts of problems + val allResearchedTechs = gameInfo.ruleSet.technologies.keys.toMutableList() + for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isDefeated() }) { + allResearchedTechs.retainAll(civ.tech.techsResearched) + } + val barbarianCiv = gameInfo.getBarbarianCivilization() + barbarianCiv.tech.techsResearched = allResearchedTechs.toHashSet() + val unitList = gameInfo.ruleSet.units.values + .filter { it.isMilitary() } + .filter { it.isBuildable(barbarianCiv) } + + var unit = if (naval) + unitList.filter { it.isWaterUnit() }.randomOrNull() + else + unitList.filter { it.isLandUnit() }.randomOrNull() + + if (unit == null) // Didn't find a unit for preferred domain + unit = unitList.randomOrNull() // Try picking another + + return unit?.name // Could still be null in case of mad modders + } + + /** When a barbarian is spawned, seed the counter for next spawn */ + private fun resetCountdown() { + // Base 8-12 turns + countdown = 8 + Random().nextInt(5) + // Quicker on Raging Barbarians + if (gameInfo.gameParameters.ragingBarbarians) + countdown /= 2 + // Higher on low difficulties + countdown += gameInfo.ruleSet.difficulties[gameInfo.gameParameters.difficulty]!!.barbarianSpawnDelay + // Quicker if this camp has already spawned units + countdown -= min(3, spawnedUnits) + + countdown *= when (gameInfo.gameParameters.gameSpeed) { + GameSpeed.Quick -> 67 + GameSpeed.Standard -> 100 + GameSpeed.Epic -> 150 + GameSpeed.Marathon -> 400 // sic! + } + countdown /= 100 + } +} \ 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 0915ebe717..4c9913cb36 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -11,17 +11,22 @@ import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.models.Religion import com.unciv.models.metadata.GameParameters +import com.unciv.models.metadata.GameSpeed import com.unciv.models.ruleset.Difficulty import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.unit.BaseUnit import java.util.* +import kotlin.math.min +import kotlin.math.pow class UncivShowableException(missingMods: String) : Exception(missingMods) class GameInfo { //region Fields - Serialized var civilizations = mutableListOf() + var barbarians = BarbarianManager() var religions: HashMap = hashMapOf() var difficulty = "Chieftain" // difficulty is game-wide, think what would happen if 2 human players could play on different difficulties? var tileMap: TileMap = TileMap() @@ -77,6 +82,7 @@ class GameInfo { val toReturn = GameInfo() toReturn.tileMap = tileMap.clone() toReturn.civilizations.addAll(civilizations.map { it.clone() }) + toReturn.barbarians = barbarians.clone() toReturn.religions.putAll(religions.map { Pair(it.key, it.value.clone()) }) toReturn.currentPlayer = currentPlayer toReturn.turns = turns @@ -112,7 +118,7 @@ class GameInfo { fun getCurrentPlayerCivilization() = currentPlayerCiv /** Get barbarian civ * @throws NoSuchElementException in no-barbarians games! */ - private fun getBarbarianCivilization() = getCivilization(Constants.barbarians) + fun getBarbarianCivilization() = getCivilization(Constants.barbarians) fun getDifficulty() = difficultyObject fun getCities() = civilizations.asSequence().flatMap { it.cities } fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() } @@ -176,9 +182,8 @@ class GameInfo { NextTurnAutomation.automateCivMoves(thisPlayer) // Placing barbarians after their turn - if (thisPlayer.isBarbarian() - && !gameParameters.noBarbarians - && turns % 10 == 0) placeBarbarians() + if (thisPlayer.isBarbarian() && !gameParameters.noBarbarians) + barbarians.updateEncampments() // exit simulation mode when player wins if (thisPlayer.victoryManager.hasWon() && simulateUntilWin) { @@ -234,80 +239,6 @@ class GameInfo { } } - - fun placeBarbarians() { - val encampments = tileMap.values.filter { it.improvement == Constants.barbarianEncampment } - - if (encampments.size < civilizations.filter { it.isMajorCiv() }.size) { - val newEncampmentTile = placeBarbarianEncampment(encampments) - if (newEncampmentTile != null) - placeBarbarianUnit(newEncampmentTile) - } - - val totalBarbariansAllowedOnMap = encampments.size * 3 - var extraBarbarians = totalBarbariansAllowedOnMap - getBarbarianCivilization().getCivUnits().count() - - for (tile in tileMap.values.filter { it.improvement == Constants.barbarianEncampment }) { - if (extraBarbarians <= 0) break - extraBarbarians-- - placeBarbarianUnit(tile) - } - } - - fun placeBarbarianEncampment(existingEncampments: List): TileInfo? { - // Barbarians will only spawn in places that no one can see - val allViewableTiles = civilizations.filterNot { it.isBarbarian() || it.isSpectator() } - .flatMap { it.viewableTiles }.toHashSet() - val tilesWithin3ofExistingEncampment = existingEncampments.asSequence() - .flatMap { it.getTilesInDistance(3) }.toSet() - val viableTiles = tileMap.values.filter { - it.isLand && it.terrainFeatures.isEmpty() - && !it.isImpassible() - && it !in tilesWithin3ofExistingEncampment - && it !in allViewableTiles - } - if (viableTiles.isEmpty()) return null // no place for more barbs =( - val tile = viableTiles.random() - tile.improvement = Constants.barbarianEncampment - notifyCivsOfBarbarianEncampment(tile) - return tile - } - - private fun placeBarbarianUnit(tileToPlace: TileInfo) { - // if we don't make this into a separate list then the retain() will happen on the Tech keys, - // which effectively removes those techs from the game and causes all sorts of problems - val allResearchedTechs = ruleSet.technologies.keys.toMutableList() - for (civ in civilizations.filter { !it.isBarbarian() && !it.isDefeated() }) { - allResearchedTechs.retainAll(civ.tech.techsResearched) - } - val barbarianCiv = getBarbarianCivilization() - barbarianCiv.tech.techsResearched = allResearchedTechs.toHashSet() - val unitList = ruleSet.units.values - .filter { it.isMilitary() } - .filter { it.isBuildable(barbarianCiv) } - - val landUnits = unitList.filter { it.isLandUnit() } - val waterUnits = unitList.filter { it.isWaterUnit() } - - val unit: String = if (waterUnits.isNotEmpty() && tileToPlace.isCoastalTile() && Random().nextBoolean()) - waterUnits.random().name - else landUnits.random().name - - tileMap.placeUnitNearTile(tileToPlace.position, unit, getBarbarianCivilization()) - } - - /** - * [CivilizationInfo.addNotification][Add a notification] to every civilization that have - * adopted Honor policy and have explored the [tile] where the Barbarian Encampment has spawned. - */ - private fun notifyCivsOfBarbarianEncampment(tile: TileInfo) { - civilizations.filter { - it.hasUnique("Notified of new Barbarian encampments") - && it.exploredTiles.contains(tile.position) - } - .forEach { it.addNotification("A new barbarian encampment has spawned!", tile.position, NotificationIcon.War) } - } - // All cross-game data which needs to be altered (e.g. when removing or changing a name of a building/tech) // will be done here, and not in CivInfo.setTransients or CityInfo fun setTransients() { @@ -374,6 +305,8 @@ class GameInfo { civInfo.hasEverOwnedOriginalCapital = civInfo.cities.any { it.isOriginalCapital } } } + + barbarians.setTransients(this) } //endregion diff --git a/core/src/com/unciv/logic/automation/BarbarianAutomation.kt b/core/src/com/unciv/logic/automation/BarbarianAutomation.kt index 7d46dd50b0..cf64e7bae1 100644 --- a/core/src/com/unciv/logic/automation/BarbarianAutomation.kt +++ b/core/src/com/unciv/logic/automation/BarbarianAutomation.kt @@ -22,8 +22,8 @@ class BarbarianAutomation(val civInfo: CivilizationInfo) { // 1 - trying to upgrade if (UnitAutomation.tryUpgradeUnit(unit)) return - // 2 - trying to attack somebody - if (BattleHelper.tryAttackNearbyEnemy(unit)) return + // 2 - trying to attack somebody - but don't leave the encampment + if (BattleHelper.tryAttackNearbyEnemy(unit, stayOnTile = true)) return // 3 - at least fortifying unit.fortifyIfCan() diff --git a/core/src/com/unciv/logic/automation/BattleHelper.kt b/core/src/com/unciv/logic/automation/BattleHelper.kt index d20a2d0f6a..67842c5cdc 100644 --- a/core/src/com/unciv/logic/automation/BattleHelper.kt +++ b/core/src/com/unciv/logic/automation/BattleHelper.kt @@ -11,9 +11,9 @@ import com.unciv.models.AttackableTile object BattleHelper { - fun tryAttackNearbyEnemy(unit: MapUnit): Boolean { + fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean { if (unit.hasUnique("Cannot attack")) return false - val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles()) + val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile) // Only take enemies we can fight without dying .filter { BattleDamage.calculateDamageToAttacker(MapUnitCombatant(unit), @@ -33,7 +33,8 @@ object BattleHelper { fun getAttackableEnemies( unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn, - tilesToCheck: List? = null + tilesToCheck: List? = null, + stayOnTile: Boolean = false ): ArrayList { val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles) .filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) } @@ -48,7 +49,7 @@ object BattleHelper { // Silly floats, basically val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack") - val tilesToAttackFrom = if (unit.baseUnit.movesLikeAirUnits()) sequenceOf(unit.currentTile) + val tilesToAttackFrom = if (stayOnTile || unit.baseUnit.movesLikeAirUnits()) sequenceOf(unit.currentTile) else unitDistanceToTiles.asSequence() .filter { diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 2c2381580e..583482c17c 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -304,14 +304,19 @@ object Battle { } private fun postBattleNationUniques(defender: ICombatant, attackedTile: TileInfo, attacker: ICombatant) { - // German unique - needs to be checked before we try to move to the enemy tile, since the encampment disappears after we move in - if (defender.isDefeated() && defender.getCivInfo().isBarbarian() - && attackedTile.improvement == Constants.barbarianEncampment - && attacker.getCivInfo().hasUnique("67% chance to earn 25 Gold and recruit a Barbarian unit from a conquered encampment") - && Random().nextDouble() < 0.67) { - attacker.getCivInfo().placeUnitNearTile(attackedTile.position, defender.getName()) - attacker.getCivInfo().addGold(25) - attacker.getCivInfo().addNotification("A barbarian [${defender.getName()}] has joined us!", attackedTile.position, defender.getName()) + + // Barbarians reduce spawn countdown after their camp was attacked "kicking the hornet's nest" + if (defender.getCivInfo().isBarbarian() && attackedTile.improvement == Constants.barbarianEncampment) { + defender.getCivInfo().gameInfo.barbarians.campAttacked(attackedTile.position) + + // German unique - needs to be checked before we try to move to the enemy tile, since the encampment disappears after we move in + if (defender.isDefeated() + && attacker.getCivInfo().hasUnique("67% chance to earn 25 Gold and recruit a Barbarian unit from a conquered encampment") + && Random().nextDouble() < 0.67) { + attacker.getCivInfo().placeUnitNearTile(attackedTile.position, defender.getName()) + attacker.getCivInfo().addGold(25) + attacker.getCivInfo().addNotification("A barbarian [${defender.getName()}] has joined us!", attackedTile.position, defender.getName()) + } } // Similarly, Ottoman unique diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index f38d18f0f7..3ad9d7d900 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -419,6 +419,10 @@ open class TileInfo { RoadStatus.values().none { it.name == improvement.name || it.removeAction == improvement.name } && getTileImprovement().let { it != null && it.hasUnique("Irremovable") } -> false + // Terrain blocks most improvements + getAllTerrains().any { it.getMatchingUniques("Only [] improvements may be built on this tile") + .any { unique -> !improvement.matchesFilter(unique.params[0]) } } -> false + // Decide cancelImprovementOrder earlier, otherwise next check breaks it improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null) // Tiles with no terrains, and no turns to build, are like great improvements - they're placeable diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index f990745b8f..3b32fe1ca2 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -18,6 +18,7 @@ class GameParameters { // Default values are the default new game var numberOfCityStates = 6 var noBarbarians = false + var ragingBarbarians = false var oneCityChallenge = false var godMode = false var nuclearWeaponsEnabled = true @@ -38,6 +39,7 @@ class GameParameters { // Default values are the default new game parameters.players = ArrayList(players) parameters.numberOfCityStates = numberOfCityStates parameters.noBarbarians = noBarbarians + parameters.ragingBarbarians = ragingBarbarians parameters.oneCityChallenge = oneCityChallenge parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled parameters.religionEnabled = religionEnabled @@ -57,6 +59,7 @@ class GameParameters { // Default values are the default new game yield("$numberOfCityStates CS") if (isOnlineMultiplayer) yield("Online Multiplayer") if (noBarbarians) yield("No barbs") + if (ragingBarbarians) yield("Raging barbs") if (oneCityChallenge) yield("OCC") if (!nuclearWeaponsEnabled) yield("No nukes") if (religionEnabled) yield("Religion") diff --git a/core/src/com/unciv/models/ruleset/Difficulty.kt b/core/src/com/unciv/models/ruleset/Difficulty.kt index 5030f42fea..1808423bc9 100644 --- a/core/src/com/unciv/models/ruleset/Difficulty.kt +++ b/core/src/com/unciv/models/ruleset/Difficulty.kt @@ -18,6 +18,7 @@ class Difficulty: INamed, ICivilopediaText { var policyCostModifier:Float = 1f var unhappinessModifier:Float = 1f var barbarianBonus:Float = 0f + var barbarianSpawnDelay: Int = 0 var playerBonusStartingUnits = ArrayList() var aiCityGrowthModifier:Float = 1f @@ -54,6 +55,7 @@ class Difficulty: INamed, ICivilopediaText { lines += FormattedLine("{Policy cost modifier}: ${policyCostModifier.toPercent()}% ${Fonts.culture}", indent = 1) lines += FormattedLine("{Unhappiness modifier}: ${unhappinessModifier.toPercent()}%", indent = 1) lines += FormattedLine("{Bonus vs. Barbarians}: ${barbarianBonus.toPercent()}% ${Fonts.strength}", indent = 1) + lines += FormattedLine("{Barbarian spawning delay}: ${barbarianSpawnDelay}", indent = 1) if (playerBonusStartingUnits.isNotEmpty()) { lines += FormattedLine() diff --git a/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt index c71e73763c..746eebf690 100644 --- a/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt @@ -44,7 +44,8 @@ class GameOptionsTable( addVictoryTypeCheckboxes() val checkboxTable = Table().apply { defaults().left().pad(2.5f) } - checkboxTable.addBarbariansCheckbox() + checkboxTable.addNoBarbariansCheckbox() + checkboxTable.addRagingBarbariansCheckbox() checkboxTable.addOneCityChallengeCheckbox() checkboxTable.addNuclearWeaponsCheckbox() checkboxTable.addIsOnlineMultiplayerCheckbox() @@ -63,10 +64,14 @@ class GameOptionsTable( add(checkbox).colspan(2).row() } - private fun Table.addBarbariansCheckbox() = + private fun Table.addNoBarbariansCheckbox() = addCheckbox("No Barbarians", gameParameters.noBarbarians) { gameParameters.noBarbarians = it } + private fun Table.addRagingBarbariansCheckbox() = + addCheckbox("Raging Barbarians", gameParameters.ragingBarbarians) + { gameParameters.ragingBarbarians = it } + private fun Table.addOneCityChallengeCheckbox() = addCheckbox("One City Challenge", gameParameters.oneCityChallenge) { gameParameters.oneCityChallenge = it }