Implemented Inquisitors (#4909)

* Added inquisitor unit including image

* Inquisitor now blocks spreading of religions

* Added 'remove heresy' action

* Fixed tests

* Reworded remove heresy unique, updated sprites

* Fix Crash

* Implemented requested changes & fixed a few minor bugs

* Implemented requested changes
This commit is contained in:
Xander Lenstra
2021-08-23 19:26:37 +02:00
committed by GitHub
parent df1695f782
commit 89ea30af95
26 changed files with 960 additions and 841 deletions

View File

@ -9,6 +9,8 @@ object Constants {
const val settler = "Settler"
const val settlerUnique = "Founds a new city"
const val eraSpecificUnit = "Era Starting Unit"
const val spreadReligionAbilityCount = "Spread Religion"
const val removeHeresyAbilityCount = "Remove Foreign religions from your own cities"
const val hiddenWithoutReligionUnique = "Hidden when religion is disabled"
const val hideFromCivilopediaUnique = "Will not be displayed in Civilopedia"

View File

@ -444,13 +444,6 @@ object Battle {
}
private fun captureCivilianUnit(attacker: ICombatant, defender: MapUnitCombatant, checkDefeat: Boolean = true) {
// barbarians don't capture civilians
if (attacker.getCivInfo().isBarbarian()
|| defender.unit.hasUnique("Uncapturable")) {
defender.takeDamage(100)
return
}
// need to save this because if the unit is captured its owner wil be overwritten
val defenderCiv = defender.getCivInfo()
@ -460,21 +453,28 @@ object Battle {
val capturedUnitTile = capturedUnit.getTile()
// Apparently in Civ V, captured settlers are converted to workers.
if (capturedUnit.name == Constants.settler) {
capturedUnit.destroy()
// This is so that future checks which check if a unit has been captured are caught give the right answer
// For example, in postBattleMoveToAttackedTile
capturedUnit.civInfo = attacker.getCivInfo()
attacker.getCivInfo().placeUnitNearTile(capturedUnitTile.position, Constants.worker)
} else {
capturedUnit.civInfo.removeUnit(capturedUnit)
capturedUnit.assignOwner(attacker.getCivInfo())
capturedUnit.currentMovement = 0f
// It's possible that the unit can no longer stand on the tile it was captured on.
// For example, because it's embarked and the capturing civ cannot embark units yet.
if (!capturedUnit.movement.canPassThrough(capturedUnitTile)) {
capturedUnit.movement.teleportToClosestMoveableTile()
when {
// Uncapturable units are destroyed (units captured by barbarians also - for now)
defender.unit.hasUnique("Uncapturable") || attacker.getCivInfo().isBarbarian() -> {
capturedUnit.destroy()
}
// Captured settlers are converted to workers.
capturedUnit.name == Constants.settler -> {
capturedUnit.destroy()
// This is so that future checks which check if a unit has been captured are caught give the right answer
// For example, in postBattleMoveToAttackedTile
capturedUnit.civInfo = attacker.getCivInfo()
attacker.getCivInfo().placeUnitNearTile(capturedUnitTile.position, Constants.worker)
}
else -> {
capturedUnit.civInfo.removeUnit(capturedUnit)
capturedUnit.assignOwner(attacker.getCivInfo())
capturedUnit.currentMovement = 0f
// It's possible that the unit can no longer stand on the tile it was captured on.
// For example, because it's embarked and the capturing civ cannot embark units yet.
if (!capturedUnit.movement.canPassThrough(capturedUnitTile)) {
capturedUnit.movement.teleportToClosestMoveableTile()
}
}
}

View File

@ -683,7 +683,6 @@ class CityInfo {
}
fun getImprovableTiles(): Sequence<TileInfo> = getTiles()
.filter {it.hasViewableResource(civInfo) && it.improvement == null}
//endregion
}

View File

@ -16,7 +16,7 @@ class CityInfoReligionManager {
val religionsAtSomePointAdopted: HashSet<String> = hashSetOf()
private val pressures: Counter<String> = Counter()
// `getNumberOfFollowers()` was called a surprisingly large amount of time, so caching it feels useful
// Cached because using `updateNumberOfFollowers` to get this value resulted in many calls
@Transient
private val followers: Counter<String> = Counter()
@ -68,6 +68,10 @@ class CityInfoReligionManager {
return getUniques().filter { it.placeholderText == unique }
}
fun getPressures(): Counter<String> {
return pressures.clone()
}
fun clearAllPressures() {
pressures.clear()
// We add pressure for following no religion
@ -85,6 +89,13 @@ class CityInfoReligionManager {
updateNumberOfFollowers(shouldUpdateFollowers)
}
}
fun removeAllPressuresExceptFor(religion: String) {
val pressureFromThisReligion = pressures[religion]!!
clearAllPressures()
pressures.add(religion, pressureFromThisReligion)
updateNumberOfFollowers()
}
fun updatePressureOnPopulationChange(populationChangeAmount: Int) {
val majorityReligion =
@ -204,9 +215,12 @@ class CityInfoReligionManager {
fun getMajorityReligionName(): String? {
if (followers.isEmpty()) return null
val religionWithMaxFollowers = followers.maxByOrNull { it.value }!!
return if (religionWithMaxFollowers.value >= cityInfo.population.population / 2) religionWithMaxFollowers.key
else null
val religionWithMaxPressure = pressures.maxByOrNull { it.value }!!.key
return when {
religionWithMaxPressure == Constants.noReligionName -> null
followers[religionWithMaxPressure]!! >= cityInfo.population.population / 2 -> religionWithMaxPressure
else -> null
}
}
fun getMajorityReligion(): Religion? {

View File

@ -1,6 +1,7 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException
@ -803,8 +804,6 @@ class CivilizationInfo {
placedUnit.religion =
if (city != null) city.cityConstructions.cityInfo.religion.getMajorityReligionName()
else religionManager.religion?.name
if (placedUnit.hasUnique("Can spread religion [] times"))
placedUnit.abilityUsedCount["Religion Spread"] = 0
}
return placedUnit

View File

@ -1,5 +1,6 @@
package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.logic.map.MapUnit
import com.unciv.models.Religion
import com.unciv.models.ruleset.Belief
@ -125,7 +126,7 @@ class ReligionManager {
fun mayFoundReligionAtAll(prophet: MapUnit): Boolean {
if (religion == null) return false // First found a pantheon
if (religion!!.isMajorReligion()) return false // Already created a major religion
if (prophet.abilityUsedCount["Religion Spread"] != 0) return false // Already used its power for other things
if (prophet.abilityUsedCount.any { it.value != 0 }) return false // Already used its power for other things
if (!civInfo.isMajorCiv()) return false // Only major civs may use religion
val foundedReligionsCount = civInfo.gameInfo.civilizations.count {

View File

@ -446,6 +446,14 @@ class MapUnit {
promotions.setTransients(this)
baseUnit = ruleset.units[name]
?: throw java.lang.Exception("Unit $name is not found!")
// "Religion Spread" ability deprecated since 3.16.7, replaced with "Spread Religion"
if ("Religion Spread" in abilityUsedCount) {
abilityUsedCount[Constants.spreadReligionAbilityCount] = abilityUsedCount["Religion Spread"]!!
abilityUsedCount.remove("Religion Spread")
}
//
updateUniques()
}
@ -955,22 +963,29 @@ class MapUnit {
return matchingUniques.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
}
fun maxReligionSpreads(): Int {
return getMatchingUniques("Can spread religion [] times").sumBy { it.params[0].toInt() }
fun religiousActionsUnitCanDo(): Sequence<String> {
return getMatchingUniques("Can [] [] times")
.map { it.params[0] }
}
fun canSpreadReligion(): Boolean {
return hasUnique("Can spread religion [] times")
fun canDoReligiousAction(action: String): Boolean {
return getMatchingUniques("Can [] [] times").any { it.params[0] == action }
}
fun getMaxReligiousActionUses(action: String): Int {
return getMatchingUniques("Can [] [] times")
.filter { it.params[0] == action }
.sumBy { it.params[1].toInt() }
}
fun getPressureAddedFromSpread(): Int {
return baseUnit.religiousStrength
}
fun getReligionString(): String {
val maxSpreads = maxReligionSpreads()
if (abilityUsedCount["Religion Spread"] == null) return "" // That is, either the key doesn't exist, or it does exist and the value is null.
return "${maxSpreads - abilityUsedCount["Religion Spread"]!!}/${maxSpreads}"
fun getActionString(action: String): String {
val maxActionUses = getMaxReligiousActionUses(action)
if (abilityUsedCount[action] == null) return "0/0" // Something went wrong
return "${maxActionUses - abilityUsedCount[action]!!}/${maxActionUses}"
}
fun actionsOnDeselect() {

View File

@ -436,6 +436,11 @@ class TileMap {
unit.promotions.addPromotion(unique.params[1], true)
}
}
// If this unit has special abilities that need to be kept track of, start doing so here
for (action in unit.religiousActionsUnitCanDo()) {
unit.abilityUsedCount[action] = 0
}
// And update civ stats, since the new unit changes both unit upkeep and resource consumption
civInfo.updateStatsForNextTurn()

View File

@ -9,6 +9,7 @@ private enum class UncivSoundConstants (val value: String) {
Chimes("chimes"),
Coin("coin"),
Choir("choir"),
Fire("fire"),
Policy("policy"),
Paper("paper"),
Whoosh("whoosh"),
@ -57,6 +58,7 @@ class UncivSound private constructor (
val Construction = UncivSound(UncivSoundConstants.Construction)
val Swap = UncivSound(UncivSoundConstants.Swap)
val Silent = UncivSound(UncivSoundConstants.Silent)
val Fire = UncivSound(UncivSoundConstants.Fire)
/** Creates an UncivSound instance for a custom sound.
* @param filename The base filename without extension.
*/

View File

@ -114,8 +114,6 @@ enum class UnitActionType(
//
Create("Create",
null, 'i', UncivSound.Chimes),
SpreadReligion("Spread Religion",
null, 'g', UncivSound.Choir),
HurryResearch("Hurry Research",
{ ImageGetter.getUnitIcon("Great Scientist") }, 'g', UncivSound.Chimes),
StartGoldenAge("Start Golden Age",
@ -128,6 +126,10 @@ enum class UnitActionType(
{ ImageGetter.getUnitIcon("Great Merchant") }, 'g', UncivSound.Chimes),
FoundReligion("Found a Religion",
{ ImageGetter.getUnitIcon("Great Prophet") }, 'g', UncivSound.Choir),
SpreadReligion("Spread Religion",
null, 'g', UncivSound.Choir),
RemoveHeresy("Remove Heresy",
{ ImageGetter.getImage("OtherIcons/Remove Heresy") }, 'h', UncivSound.Fire),
DisbandUnit("Disband unit",
{ ImageGetter.getImage("OtherIcons/DisbandUnit") }, KeyCharAndCode.DEL),
GiftUnit("Gift unit",

View File

@ -330,8 +330,6 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
if (unit.hasUnique("Religious Unit")) {
unit.religion = cityConstructions.cityInfo.religion.getMajorityReligionName()
if (unit.canSpreadReligion())
unit.abilityUsedCount["Religion Spread"] = 0
}
if (this.isCivilian()) return true // tiny optimization makes save files a few bytes smaller
@ -340,12 +338,10 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
for (unique in
// Deprecated since 3.15.9
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience") +
//
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience []")
.filter { cityConstructions.cityInfo.matchesFilter(it.params[2]) } +
// Deprecated since 3.15.9
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience") +
cityConstructions.cityInfo.getLocalMatchingUniques("New [] units start with [] Experience in this city")
//
) {

View File

@ -83,7 +83,7 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
}
private fun addReligionInfo() {
val label = cityInfo.religion.getMajorityReligionName()
val label = cityInfo.religion.getMajorityReligion()?.iconName
?: "None"
val icon = if (label == "None") "Religion" else label
val expanderTab =
@ -97,7 +97,7 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
onChange = {
pack()
// We have to re-anchor as our position in the city screen, otherwise it expands upwards.
// This probably should be refactored so its placed somewhere else in due time
// ToDo: This probably should be refactored so its placed somewhere else in due time
setPosition(stage.width - CityScreen.posFromEdge, stage.height - CityScreen.posFromEdge, Align.topRight)
}
) {

View File

@ -4,6 +4,7 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.automation.WorkerAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
@ -56,9 +57,9 @@ object UnitActions {
addGreatPersonActions(unit, actionList, tile)
addFoundReligionAction(unit, actionList, tile)
actionList += getImprovementConstructionActions(unit, tile)
addSpreadReligionActions(unit, actionList, tile)
addActionsWithLimitedUses(unit, actionList, tile)
addToggleActionsAction(unit, actionList, unitTable)
return actionList
@ -374,6 +375,7 @@ object UnitActions {
private fun addAutomateBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList<UnitAction>) {
if (!unit.hasUniqueToBuildImprovements) return
if (unit.isAutomated()) return
actionList += UnitAction(UnitActionType.Automate,
isCurrentAction = unit.isAutomated(),
@ -489,37 +491,76 @@ object UnitActions {
}.takeIf { unit.civInfo.religionManager.mayFoundReligionNow(unit) }
)
}
private fun addSpreadReligionActions(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo) {
if (!unit.hasUnique("Can spread religion [] times")) return
private fun addActionsWithLimitedUses(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo) {
val actionsToAdd = unit.religiousActionsUnitCanDo()
if (actionsToAdd.none()) return
if (unit.religion == null || unit.civInfo.gameInfo.religions[unit.religion]!!.isPantheon()) return
val maxReligionSpreads = unit.maxReligionSpreads()
if (!unit.abilityUsedCount.containsKey("Religion Spread")) return // This should be impossible anyways, but just in case
if (maxReligionSpreads <= unit.abilityUsedCount["Religion Spread"]!!) return
val city = tile.getCity() ?: return
for (action in actionsToAdd) {
if (!unit.abilityUsedCount.containsKey(action)) continue
val maxActionUses = unit.getMaxReligiousActionUses(action)
if (maxActionUses <= unit.abilityUsedCount[action]!!) continue
when (action) {
Constants.spreadReligionAbilityCount -> addSpreadReligionActions(unit, actionList, city, maxActionUses)
Constants.removeHeresyAbilityCount -> addRemoveHeresyActions(unit, actionList, city, maxActionUses)
}
}
}
private fun useActionWithLimitedUses(unit: MapUnit, action: String, maximumUses: Int) {
unit.abilityUsedCount[action] = unit.abilityUsedCount[action]!! + 1
if (unit.abilityUsedCount[action] == maximumUses) {
if (unit.isGreatPerson())
addGoldPerGreatPersonUsage(unit.civInfo)
unit.destroy()
}
}
private fun addSpreadReligionActions(unit: MapUnit, actionList: ArrayList<UnitAction>, city: CityInfo, maxSpreadUses: Int) {
val blockedByInquisitor =
city.getCenterTile()
.getTilesInDistance(1)
.flatMap { it.getUnits() }
.any {
it.hasUnique("Prevents spreading of religion to the city it is next to")
&& it.religion != unit.religion
}
actionList += UnitAction(UnitActionType.SpreadReligion,
title = "Spread [${unit.religion!!}]",
action = {
unit.abilityUsedCount["Religion Spread"] = unit.abilityUsedCount["Religion Spread"]!! + 1
val followersOfOtherReligions = city.religion.getFollowersOfOtherReligionsThan(unit.religion!!)
for (unique in unit.civInfo.getMatchingUniques("When spreading religion to a city, gain [] times the amount of followers of other religions as []")) {
unit.civInfo.addStat(Stat.valueOf(unique.params[1]), followersOfOtherReligions * unique.params[0].toInt())
}
city.religion.addPressure(unit.religion!!, unit.getPressureAddedFromSpread())
unit.currentMovement = 0f
if (unit.abilityUsedCount["Religion Spread"] == maxReligionSpreads) {
addGoldPerGreatPersonUsage(unit.civInfo)
unit.destroy()
}
}.takeIf { unit.currentMovement > 0 }
useActionWithLimitedUses(unit, Constants.spreadReligionAbilityCount, maxSpreadUses)
}.takeIf { unit.currentMovement > 0 && !blockedByInquisitor }
)
}
private fun addRemoveHeresyActions(unit: MapUnit, actionList: ArrayList<UnitAction>, city: CityInfo, maxHerseyUses: Int) {
if (city.civInfo != unit.civInfo) return
// Only allow the action if the city actually has any foreign religion
// This will almost be always due to pressure from cities close-by
if (city.religion.getPressures().none { it.key != unit.religion!! }) return
actionList += UnitAction(UnitActionType.RemoveHeresy,
title = "Remove Heresy",
action = {
city.religion.removeAllPressuresExceptFor(unit.religion!!)
unit.currentMovement = 0f
useActionWithLimitedUses(unit, Constants.removeHeresyAbilityCount, maxHerseyUses)
}.takeIf { unit.currentMovement > 0f }
)
}
fun getImprovementConstructionActions(unit: MapUnit, tile: TileInfo): ArrayList<UnitAction> {
val finalActions = ArrayList<UnitAction>()
var uniquesToCheck = unit.getMatchingUniques("Can construct []")
if (unit.abilityUsedCount.containsKey("Religion Spread") && unit.abilityUsedCount["Religion Spread"]!! == 0 && unit.canSpreadReligion())
uniquesToCheck += unit.getMatchingUniques("Can construct [] if it hasn't spread religion yet")
if (unit.religiousActionsUnitCanDo().all { unit.abilityUsedCount[it] == 0 })
uniquesToCheck += unit.getMatchingUniques("Can construct [] if it hasn't used other actions yet")
for (unique in uniquesToCheck) {
val improvementName = unique.params[0]
val improvement = tile.ruleset.tileImprovements[improvementName]

View File

@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.city.CityInfo
@ -163,9 +164,14 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
unitDescriptionTable.add(unit.promotions.XP.toString() + "/" + unit.promotions.xpForNextPromotion())
}
if (unit.canSpreadReligion()) {
if (unit.canDoReligiousAction(Constants.spreadReligionAbilityCount)) {
unitDescriptionTable.add(ImageGetter.getStatIcon("Faith")).size(20f)
unitDescriptionTable.add(unit.getReligionString())
unitDescriptionTable.add(unit.getActionString(Constants.spreadReligionAbilityCount))
}
if (unit.canDoReligiousAction(Constants.removeHeresyAbilityCount)) {
unitDescriptionTable.add(ImageGetter.getImage("OtherIcons/Remove Heresy")).size(20f)
unitDescriptionTable.add(unit.getActionString(Constants.removeHeresyAbilityCount))
}
if (unit.baseUnit.religiousStrength > 0) {