mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 14:24:43 -04:00
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:
parent
344c96319b
commit
9016385f30
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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 =
|
||||||
|
271
core/src/com/unciv/logic/BarbarianManager.kt
Normal file
271
core/src/com/unciv/logic/BarbarianManager.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
@ -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 }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user