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
This commit is contained in:
SimonCeder 2021-10-03 10:45:02 +02:00 committed by GitHub
parent 344c96319b
commit 9016385f30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 330 additions and 96 deletions

View File

@ -11,6 +11,7 @@
"policyCostModifier": 0.5, "policyCostModifier": 0.5,
"unhappinessModifier": 0.4, "unhappinessModifier": 0.4,
"barbarianBonus": 0.75, "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. "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 "aiCityGrowthModifier": 1.6, // that is to say it'll take them 1.6 times as long to grow the city
"aiUnitCostModifier": 1.75, "aiUnitCostModifier": 1.75,
@ -39,6 +40,7 @@
"policyCostModifier": 0.67, "policyCostModifier": 0.67,
"unhappinessModifier": 0.6, "unhappinessModifier": 0.6,
"barbarianBonus": 0.5, "barbarianBonus": 0.5,
"barbarianSpawnDelay": 5,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 1.3, "aiCityGrowthModifier": 1.3,
"aiUnitCostModifier": 1.3, "aiUnitCostModifier": 1.3,
@ -67,6 +69,7 @@
"policyCostModifier": 0.85, "policyCostModifier": 0.85,
"unhappinessModifier": 0.75, "unhappinessModifier": 0.75,
"barbarianBonus": 0.4, "barbarianBonus": 0.4,
"barbarianSpawnDelay": 3,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 1.1, "aiCityGrowthModifier": 1.1,
"aiUnitCostModifier": 1.1, "aiUnitCostModifier": 1.1,
@ -95,6 +98,7 @@
"policyCostModifier": 1, "policyCostModifier": 1,
"unhappinessModifier": 1, "unhappinessModifier": 1,
"barbarianBonus": 0.33, "barbarianBonus": 0.33,
"barbarianSpawnDelay": 0,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 1, "aiCityGrowthModifier": 1,
"aiUnitCostModifier": 1, "aiUnitCostModifier": 1,
@ -123,6 +127,7 @@
"policyCostModifier": 1, "policyCostModifier": 1,
"unhappinessModifier": 1, "unhappinessModifier": 1,
"barbarianBonus": 0.25, "barbarianBonus": 0.25,
"barbarianSpawnDelay": 0,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 0.9, "aiCityGrowthModifier": 0.9,
"aiUnitCostModifier": 0.85, "aiUnitCostModifier": 0.85,
@ -151,6 +156,7 @@
"policyCostModifier": 1, "policyCostModifier": 1,
"unhappinessModifier": 1, "unhappinessModifier": 1,
"barbarianBonus": 0.2, "barbarianBonus": 0.2,
"barbarianSpawnDelay": 0,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 0.85, "aiCityGrowthModifier": 0.85,
"aiUnitCostModifier": 0.8, "aiUnitCostModifier": 0.8,
@ -179,6 +185,7 @@
"policyCostModifier": 1, "policyCostModifier": 1,
"unhappinessModifier": 1, "unhappinessModifier": 1,
"barbarianBonus": 0.1, "barbarianBonus": 0.1,
"barbarianSpawnDelay": 0,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 0.75, "aiCityGrowthModifier": 0.75,
"aiUnitCostModifier": 0.65, "aiUnitCostModifier": 0.65,
@ -207,6 +214,7 @@
"policyCostModifier": 1, "policyCostModifier": 1,
"unhappinessModifier": 1, "unhappinessModifier": 1,
"barbarianBonus": 0, "barbarianBonus": 0,
"barbarianSpawnDelay": 0,
"playerBonusStartingUnits": [], "playerBonusStartingUnits": [],
"aiCityGrowthModifier": 0.6, "aiCityGrowthModifier": 0.6,
"aiUnitCostModifier": 0.5, "aiUnitCostModifier": 0.5,

View File

@ -157,7 +157,7 @@
"gold": -3, "gold": -3,
"movementCost": 2, "movementCost": 2,
"unbuildable": true, "unbuildable": true,
"defenceBonus": -0.15 "defenceBonus": -0.15,
}, },
{ {
"name": "Oasis", "name": "Oasis",
@ -168,7 +168,7 @@
"unbuildable": true, "unbuildable": true,
"defenceBonus": -0.1, "defenceBonus": -0.1,
"occursOn": ["Desert"], "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", "name": "Flood plains",

View File

@ -308,6 +308,7 @@ Archipelago =
Number of City-States = Number of City-States =
One City Challenge = One City Challenge =
No Barbarians = No Barbarians =
Raging Barbarians =
No Ancient Ruins = No Ancient Ruins =
No Natural Wonders = No Natural Wonders =
Victory Conditions = Victory Conditions =
@ -1037,6 +1038,7 @@ Building cost modifier =
Policy cost modifier = Policy cost modifier =
Unhappiness modifier = Unhappiness modifier =
Bonus vs. Barbarians = Bonus vs. Barbarians =
Barbarian spawning delay =
Bonus starting units = Bonus starting units =
AI settings = AI settings =

View File

@ -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<Vector2, Encampment>()
@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
}
}

View File

@ -11,17 +11,22 @@ import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.Religion import com.unciv.models.Religion
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.Difficulty import com.unciv.models.ruleset.Difficulty
import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unit.BaseUnit
import java.util.* import java.util.*
import kotlin.math.min
import kotlin.math.pow
class UncivShowableException(missingMods: String) : Exception(missingMods) class UncivShowableException(missingMods: String) : Exception(missingMods)
class GameInfo { class GameInfo {
//region Fields - Serialized //region Fields - Serialized
var civilizations = mutableListOf<CivilizationInfo>() var civilizations = mutableListOf<CivilizationInfo>()
var barbarians = BarbarianManager()
var religions: HashMap<String, Religion> = hashMapOf() var religions: HashMap<String, Religion> = hashMapOf()
var difficulty = "Chieftain" // difficulty is game-wide, think what would happen if 2 human players could play on different difficulties? var difficulty = "Chieftain" // difficulty is game-wide, think what would happen if 2 human players could play on different difficulties?
var tileMap: TileMap = TileMap() var tileMap: TileMap = TileMap()
@ -77,6 +82,7 @@ class GameInfo {
val toReturn = GameInfo() val toReturn = GameInfo()
toReturn.tileMap = tileMap.clone() toReturn.tileMap = tileMap.clone()
toReturn.civilizations.addAll(civilizations.map { it.clone() }) toReturn.civilizations.addAll(civilizations.map { it.clone() })
toReturn.barbarians = barbarians.clone()
toReturn.religions.putAll(religions.map { Pair(it.key, it.value.clone()) }) toReturn.religions.putAll(religions.map { Pair(it.key, it.value.clone()) })
toReturn.currentPlayer = currentPlayer toReturn.currentPlayer = currentPlayer
toReturn.turns = turns toReturn.turns = turns
@ -112,7 +118,7 @@ class GameInfo {
fun getCurrentPlayerCivilization() = currentPlayerCiv fun getCurrentPlayerCivilization() = currentPlayerCiv
/** Get barbarian civ /** Get barbarian civ
* @throws NoSuchElementException in no-barbarians games! */ * @throws NoSuchElementException in no-barbarians games! */
private fun getBarbarianCivilization() = getCivilization(Constants.barbarians) fun getBarbarianCivilization() = getCivilization(Constants.barbarians)
fun getDifficulty() = difficultyObject fun getDifficulty() = difficultyObject
fun getCities() = civilizations.asSequence().flatMap { it.cities } fun getCities() = civilizations.asSequence().flatMap { it.cities }
fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() } fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() }
@ -176,9 +182,8 @@ class GameInfo {
NextTurnAutomation.automateCivMoves(thisPlayer) NextTurnAutomation.automateCivMoves(thisPlayer)
// Placing barbarians after their turn // Placing barbarians after their turn
if (thisPlayer.isBarbarian() if (thisPlayer.isBarbarian() && !gameParameters.noBarbarians)
&& !gameParameters.noBarbarians barbarians.updateEncampments()
&& turns % 10 == 0) placeBarbarians()
// exit simulation mode when player wins // exit simulation mode when player wins
if (thisPlayer.victoryManager.hasWon() && simulateUntilWin) { 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>): 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) // 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 // will be done here, and not in CivInfo.setTransients or CityInfo
fun setTransients() { fun setTransients() {
@ -374,6 +305,8 @@ class GameInfo {
civInfo.hasEverOwnedOriginalCapital = civInfo.cities.any { it.isOriginalCapital } civInfo.hasEverOwnedOriginalCapital = civInfo.cities.any { it.isOriginalCapital }
} }
} }
barbarians.setTransients(this)
} }
//endregion //endregion

View File

@ -22,8 +22,8 @@ class BarbarianAutomation(val civInfo: CivilizationInfo) {
// 1 - trying to upgrade // 1 - trying to upgrade
if (UnitAutomation.tryUpgradeUnit(unit)) return if (UnitAutomation.tryUpgradeUnit(unit)) return
// 2 - trying to attack somebody // 2 - trying to attack somebody - but don't leave the encampment
if (BattleHelper.tryAttackNearbyEnemy(unit)) return if (BattleHelper.tryAttackNearbyEnemy(unit, stayOnTile = true)) return
// 3 - at least fortifying // 3 - at least fortifying
unit.fortifyIfCan() unit.fortifyIfCan()

View File

@ -11,9 +11,9 @@ import com.unciv.models.AttackableTile
object BattleHelper { object BattleHelper {
fun tryAttackNearbyEnemy(unit: MapUnit): Boolean { fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean {
if (unit.hasUnique("Cannot attack")) return false 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 // Only take enemies we can fight without dying
.filter { .filter {
BattleDamage.calculateDamageToAttacker(MapUnitCombatant(unit), BattleDamage.calculateDamageToAttacker(MapUnitCombatant(unit),
@ -33,7 +33,8 @@ object BattleHelper {
fun getAttackableEnemies( fun getAttackableEnemies(
unit: MapUnit, unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn, unitDistanceToTiles: PathsToTilesWithinTurn,
tilesToCheck: List<TileInfo>? = null tilesToCheck: List<TileInfo>? = null,
stayOnTile: Boolean = false
): ArrayList<AttackableTile> { ): ArrayList<AttackableTile> {
val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles) val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles)
.filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) } .filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) }
@ -48,7 +49,7 @@ object BattleHelper {
// Silly floats, basically // Silly floats, basically
val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack") 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 else
unitDistanceToTiles.asSequence() unitDistanceToTiles.asSequence()
.filter { .filter {

View File

@ -304,15 +304,20 @@ object Battle {
} }
private fun postBattleNationUniques(defender: ICombatant, attackedTile: TileInfo, attacker: ICombatant) { private fun postBattleNationUniques(defender: ICombatant, attackedTile: TileInfo, attacker: ICombatant) {
// 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 // 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() if (defender.isDefeated()
&& attackedTile.improvement == Constants.barbarianEncampment
&& attacker.getCivInfo().hasUnique("67% chance to earn 25 Gold and recruit a Barbarian unit from a conquered encampment") && attacker.getCivInfo().hasUnique("67% chance to earn 25 Gold and recruit a Barbarian unit from a conquered encampment")
&& Random().nextDouble() < 0.67) { && Random().nextDouble() < 0.67) {
attacker.getCivInfo().placeUnitNearTile(attackedTile.position, defender.getName()) attacker.getCivInfo().placeUnitNearTile(attackedTile.position, defender.getName())
attacker.getCivInfo().addGold(25) attacker.getCivInfo().addGold(25)
attacker.getCivInfo().addNotification("A barbarian [${defender.getName()}] has joined us!", attackedTile.position, defender.getName()) attacker.getCivInfo().addNotification("A barbarian [${defender.getName()}] has joined us!", attackedTile.position, defender.getName())
} }
}
// Similarly, Ottoman unique // Similarly, Ottoman unique
if (attacker.getCivInfo().hasUnique("50% chance of capturing defeated Barbarian naval units and earning 25 Gold") if (attacker.getCivInfo().hasUnique("50% chance of capturing defeated Barbarian naval units and earning 25 Gold")

View File

@ -419,6 +419,10 @@ open class TileInfo {
RoadStatus.values().none { it.name == improvement.name || it.removeAction == improvement.name } RoadStatus.values().none { it.name == improvement.name || it.removeAction == improvement.name }
&& getTileImprovement().let { it != null && it.hasUnique("Irremovable") } -> false && 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 // Decide cancelImprovementOrder earlier, otherwise next check breaks it
improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null) improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null)
// Tiles with no terrains, and no turns to build, are like great improvements - they're placeable // Tiles with no terrains, and no turns to build, are like great improvements - they're placeable

View File

@ -18,6 +18,7 @@ class GameParameters { // Default values are the default new game
var numberOfCityStates = 6 var numberOfCityStates = 6
var noBarbarians = false var noBarbarians = false
var ragingBarbarians = false
var oneCityChallenge = false var oneCityChallenge = false
var godMode = false var godMode = false
var nuclearWeaponsEnabled = true var nuclearWeaponsEnabled = true
@ -38,6 +39,7 @@ class GameParameters { // Default values are the default new game
parameters.players = ArrayList(players) parameters.players = ArrayList(players)
parameters.numberOfCityStates = numberOfCityStates parameters.numberOfCityStates = numberOfCityStates
parameters.noBarbarians = noBarbarians parameters.noBarbarians = noBarbarians
parameters.ragingBarbarians = ragingBarbarians
parameters.oneCityChallenge = oneCityChallenge parameters.oneCityChallenge = oneCityChallenge
parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled
parameters.religionEnabled = religionEnabled parameters.religionEnabled = religionEnabled
@ -57,6 +59,7 @@ class GameParameters { // Default values are the default new game
yield("$numberOfCityStates CS") yield("$numberOfCityStates CS")
if (isOnlineMultiplayer) yield("Online Multiplayer") if (isOnlineMultiplayer) yield("Online Multiplayer")
if (noBarbarians) yield("No barbs") if (noBarbarians) yield("No barbs")
if (ragingBarbarians) yield("Raging barbs")
if (oneCityChallenge) yield("OCC") if (oneCityChallenge) yield("OCC")
if (!nuclearWeaponsEnabled) yield("No nukes") if (!nuclearWeaponsEnabled) yield("No nukes")
if (religionEnabled) yield("Religion") if (religionEnabled) yield("Religion")

View File

@ -18,6 +18,7 @@ class Difficulty: INamed, ICivilopediaText {
var policyCostModifier:Float = 1f var policyCostModifier:Float = 1f
var unhappinessModifier:Float = 1f var unhappinessModifier:Float = 1f
var barbarianBonus:Float = 0f var barbarianBonus:Float = 0f
var barbarianSpawnDelay: Int = 0
var playerBonusStartingUnits = ArrayList<String>() var playerBonusStartingUnits = ArrayList<String>()
var aiCityGrowthModifier:Float = 1f 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("{Policy cost modifier}: ${policyCostModifier.toPercent()}% ${Fonts.culture}", indent = 1)
lines += FormattedLine("{Unhappiness modifier}: ${unhappinessModifier.toPercent()}%", indent = 1) lines += FormattedLine("{Unhappiness modifier}: ${unhappinessModifier.toPercent()}%", indent = 1)
lines += FormattedLine("{Bonus vs. Barbarians}: ${barbarianBonus.toPercent()}% ${Fonts.strength}", indent = 1) lines += FormattedLine("{Bonus vs. Barbarians}: ${barbarianBonus.toPercent()}% ${Fonts.strength}", indent = 1)
lines += FormattedLine("{Barbarian spawning delay}: ${barbarianSpawnDelay}", indent = 1)
if (playerBonusStartingUnits.isNotEmpty()) { if (playerBonusStartingUnits.isNotEmpty()) {
lines += FormattedLine() lines += FormattedLine()

View File

@ -44,7 +44,8 @@ class GameOptionsTable(
addVictoryTypeCheckboxes() addVictoryTypeCheckboxes()
val checkboxTable = Table().apply { defaults().left().pad(2.5f) } val checkboxTable = Table().apply { defaults().left().pad(2.5f) }
checkboxTable.addBarbariansCheckbox() checkboxTable.addNoBarbariansCheckbox()
checkboxTable.addRagingBarbariansCheckbox()
checkboxTable.addOneCityChallengeCheckbox() checkboxTable.addOneCityChallengeCheckbox()
checkboxTable.addNuclearWeaponsCheckbox() checkboxTable.addNuclearWeaponsCheckbox()
checkboxTable.addIsOnlineMultiplayerCheckbox() checkboxTable.addIsOnlineMultiplayerCheckbox()
@ -63,10 +64,14 @@ class GameOptionsTable(
add(checkbox).colspan(2).row() add(checkbox).colspan(2).row()
} }
private fun Table.addBarbariansCheckbox() = private fun Table.addNoBarbariansCheckbox() =
addCheckbox("No Barbarians", gameParameters.noBarbarians) addCheckbox("No Barbarians", gameParameters.noBarbarians)
{ gameParameters.noBarbarians = it } { gameParameters.noBarbarians = it }
private fun Table.addRagingBarbariansCheckbox() =
addCheckbox("Raging Barbarians", gameParameters.ragingBarbarians)
{ gameParameters.ragingBarbarians = it }
private fun Table.addOneCityChallengeCheckbox() = private fun Table.addOneCityChallengeCheckbox() =
addCheckbox("One City Challenge", gameParameters.oneCityChallenge) addCheckbox("One City Challenge", gameParameters.oneCityChallenge)
{ gameParameters.oneCityChallenge = it } { gameParameters.oneCityChallenge = it }