Moddable victories (#6161)

* Added a moddable (but not yet functional) file for victories

* Spaceship parts are units now

* Fixed tests

* Added milestone objects

* Made 'our status' table in VictoryScreen dependend on file

* Updated VictoryManager to use the new Milestone system

* Fixed bug where in vanilla too many spaceship parts could beb uild

* Whoops

* Updated global victory table to use the jsons

* Updated the new game screen to show the new victory types
Also started with the deprecation of VictorType

* Did some translation stuff, also finally fixed the tests

* Removed VictoryType and reworked AI to use Milestones instead

* Add some checks for the victory file; tested that custom victories work
Also moves some code to a better spot and fixes compilation errors

* Fixed some things I thought about while falling asleep
Most notably: built -> build; fixed spaceship part construction 
priority; removed more code for the old system

* Fixed translation issues on the victory screen
This commit is contained in:
Xander Lenstra 2022-04-24 21:45:38 +02:00 committed by GitHub
parent 382707a6a5
commit 34105efdda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 691 additions and 388 deletions

View File

@ -0,0 +1,42 @@
[
{
"name": "Scientific",
"victoryScreenHeader": "Complete all the spaceship parts\nto win!",
"milestones": [
"Build [Apollo Program]", "Add all [spaceship parts] in capital"
],
"requiredSpaceshipParts": [
"SS Engine", "SS Stasis Chamber", "SS Cockpit", "SS Booster", "SS Booster", "SS Booster"
],
"victoryString": "You have achieved victory through mastery of Science! You have conquered the mysteries of nature and led your people on a voyage to a brave new world! Your triumph will be remembered as long as the stars burn in the night sky!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Cultural",
"victoryScreenHeader": "Complete 5 policy branches and\nbuild the Utopia Project to win!",
"milestones": ["Complete [5] Policy branches", "Build [Utopia Project]"],
"victoryString": "You have achieved victory through the awesome power of your Culture. Your civilization's greatness - the magnificence of its monuments and the power of its artists - have astounded the world! Poets will honor you as long as beauty brings gladness to a weary heart.",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Domination",
"victoryScreenHeader": "Destroy all enemies\nto win!",
"milestones": ["Destroy all players"],
"victoryString": "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Diplomatic",
"hiddenInVictoryScreen": true,
"milestones": ["[United Nations] build globally", "Win diplomatic vote"],
"victoryString": "You have triumphed over your foes through the art of diplomacy! Your cunning and wisdom have earned you great friends - and divided and sown confusion among your enemies! Forever will you be remembered as the leader who brought peace to this weary world!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Time",
"hiddenInVictoryScreen": true,
"milestones": ["Have highest score after max turns"],
"victoryString": "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
}
]

View File

@ -1356,7 +1356,7 @@
"cost": 500,
"requiredTech": "Robotics",
"requiredResource": "Aluminum",
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital"]
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital", "Limited to [3] per Civilization"]
// costs 1500 in BNW
},
{
@ -1366,7 +1366,7 @@
"cost": 750,
"requiredTech": "Satellites",
"requiredResource": "Aluminum",
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital"]
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital", "Limited to [1] per Civilization"]
// costs 1500 in BNW
},
{
@ -1376,7 +1376,7 @@
"cost": 750,
"requiredTech": "Particle Physics",
"requiredResource": "Aluminum",
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital"]
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital", "Limited to [1] per Civilization"]
// costs 1500 in BNW
},
{
@ -1386,7 +1386,7 @@
"cost": 750,
"requiredTech": "Nanotechnology",
"requiredResource": "Aluminum",
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital"]
"uniques": ["Spaceship part", "Cannot be purchased", "Only available <if [Apollo Program] is constructed>", "Uncapturable", "Can be added to [The Spaceship] in the Capital", "Limited to [1] per Civilization"]
// costs 1500 in BNW
}
]

View File

@ -0,0 +1,42 @@
[
{
"name": "Scientific",
"victoryScreenHeader": "Complete all the spaceship parts\nto win!",
"milestones": [
"Build [Apollo Program]", "Add all [spaceship parts] in capital"
],
"requiredSpaceshipParts": [
"SS Engine", "SS Stasis Chamber", "SS Cockpit", "SS Booster", "SS Booster", "SS Booster"
],
"victoryString": "You have achieved victory through mastery of Science! You have conquered the mysteries of nature and led your people on a voyage to a brave new world! Your triumph will be remembered as long as the stars burn in the night sky!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Cultural",
"victoryScreenHeader": "Complete 5 policy branches and\nbuild the Utopia Project to win!",
"milestones": ["Complete [5] Policy branches", "Build [Utopia Project]"],
"victoryString": "You have achieved victory through the awesome power of your Culture. Your civilization's greatness - the magnificence of its monuments and the power of its artists - have astounded the world! Poets will honor you as long as beauty brings gladness to a weary heart.",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Domination",
"victoryScreenHeader": "Destroy all enemies\nto win!",
"milestones": ["Destroy all players"],
"victoryString": "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Diplomatic",
"hiddenInVictoryScreen": true,
"milestones": ["[United Nations] build globally", "Win diplomatic vote"],
"victoryString": "You have triumphed over your foes through the art of diplomacy! Your cunning and wisdom have earned you great friends - and divided and sown confusion among your enemies! Forever will you be remembered as the leader who brought peace to this weary world!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
},
{
"name": "Time",
"hiddenInVictoryScreen": true,
"milestones": ["Have highest score after max turns"],
"victoryString": "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!",
"defeatString": "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
}
]

View File

@ -415,6 +415,8 @@ Starting Era =
It looks like we can't make a map with the parameters you requested! =
Maybe you put too many players into too small a map? =
No human players selected! =
Invalid Player ID! =
No victory conditions were selected! =
Mods: =
Extension mods: =
Base ruleset: =
@ -1078,31 +1080,22 @@ Number of your cities\ndemanding this resource for\n'We Love The King Day' =
# Victory
Science victory =
Cultural victory =
Conquest victory =
Diplomatic victory =
Complete all the spaceship parts\n to win! =
Complete 5 policy branches\n to win! =
Complete 5 policy branches and build\n the Utopia Project to win! =
Destroy all enemies\n to win! =
[victoryType] Victory =
Built [building] =
Add all spaceship parts in capital =
Destroy all players =
Capture all capitals =
Complete [amount] Policy branche =
You have won a [victoryType] Victory! =
You have achieved victory through the awesome power of your Culture. Your civilization's greatness - the magnificence of its monuments and the power of its artists - have astounded the world! Poets will honor you as long as beauty brings gladness to a weary heart. =
The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph! =
You have achieved victory through mastery of Science! You have conquered the mysteries of nature and led your people on a voyage to a brave new world! Your triumph will be remembered as long as the stars burn in the night sky! =
[civilization] has won a [victoryType] Victory! =
Your civilization stands above all others! The exploits of your people shall be remembered until the end of civilization itself! =
You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory! =
You have triumphed over your foes through the art of diplomacy! Your cunning and wisdom have earned you great friends - and divided and sown confusion among your enemies! Forever will you be remembered as the leader who brought peace to this weary world! =
The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph! =
One more turn...! =
Built Apollo Program =
Destroy [civName] =
Capture [cityName] =
Our status =
Global status =
Rankings =
Spaceship parts remaining =
Branches completed =
Undefeated civs =
# The \n here means: put a newline (enter) here. If this is omitted, the sidebox in the diplomacy overview will become _really_ wide.
# Feel free to replace it with a space and put it between other words in your translation
Turns until the next\ndiplomacy victory vote: [amount] =

View File

@ -47,9 +47,11 @@ object Constants {
const val citadel = "Citadel"
const val futureTech = "Future Tech"
// Easter egg name. Hopefully is to hopefully avoid conflicts when later players can name their own religions.
// Easter egg name. Is to avoid conflicts when players name their own religions.
// This religion name should never be displayed.
const val noReligionName = "The religion of TheLegend27"
const val neutralVictoryType = "Neutral"
const val cancelImprovementOrder = "Cancel improvement order"
const val tutorialPopupNamePrefix = "Tutorial: "

View File

@ -12,7 +12,7 @@ class JsonParser {
fun <T> getFromJson(tClass: Class<T>, filePath: String): T = getFromJson(tClass, Gdx.files.internal(filePath))
fun <T> getFromJson(tClass: Class<T>, file: FileHandle): T {
try{
try {
val jsonText = file.readString(Charsets.UTF_8.name())
return json.fromJson(tClass, jsonText)
} catch (exception:Exception){

View File

@ -37,6 +37,8 @@ class GameInfo {
// Maps a civ to the civ they voted for
var diplomaticVictoryVotesCast = HashMap<String, String>()
// Set to false whenever the results still need te be processed
var diplomaticVictoryVotesProcessed = false
/**Keep track of a custom location this game was saved to _or_ loaded from
*
@ -205,7 +207,6 @@ class GameInfo {
currentPlayerIndex = (currentPlayerIndex + 1) % civilizations.size
if (currentPlayerIndex == 0) {
turns++
checkForTimeVictory()
}
thisPlayer = civilizations[currentPlayerIndex]
thisPlayer.startTurn()
@ -266,7 +267,7 @@ class GameInfo {
&& (!it.militaryUnit!!.isInvisible(thisPlayer) || viewableInvisibleTiles.contains(it.position)))
}
// enemy units ON our territory
// enemy units IN our territory
addEnemyUnitNotification(thisPlayer,
enemyUnitsCloseToTerritory.filter { it.getOwner() == thisPlayer },
"in"
@ -283,16 +284,17 @@ class GameInfo {
}
)
}
fun getEnabledVictories() = ruleSet.victories.filter { !it.value.hiddenInVictoryScreen && gameParameters.victoryTypes.contains(it.key) }
private fun checkForTimeVictory() {
if (turns != gameParameters.maxTurns || !gameParameters.victoryTypes.contains(VictoryType.Time)) return
val winningCiv = civilizations
.filter { it.isMajorCiv() && !it.isSpectator() && !it.isBarbarian() }
.maxByOrNull { it.calculateTotalScore() }
?: return // Are there no civs left?
winningCiv.victoryManager.hasWonTimeVictory = true
fun processDiplomaticVictory() {
if (diplomaticVictoryVotesProcessed) return
for (civInfo in civilizations) {
if (civInfo.victoryManager.hasEnoughVotesForDiplomaticVictory()) {
civInfo.victoryManager.hasEverWonDiplomaticVote = true
}
}
diplomaticVictoryVotesProcessed = true
}
private fun addEnemyUnitNotification(thisPlayer: CivilizationInfo, tiles: List<TileInfo>, inOrNear: String) {
@ -450,8 +452,10 @@ class GameInfo {
}
}
spaceResources.clear()
spaceResources.addAll(ruleSet.buildings.values.filter { it.hasUnique(UniqueType.SpaceshipPart) }
.flatMap { it.getResourceRequirements().keys } )
spaceResources.addAll(ruleSet.victories.values.flatMap { it.requiredSpaceshipParts })
barbarians.setTransients(this)

View File

@ -8,7 +8,8 @@ import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.ThingToFocus
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
@ -41,7 +42,7 @@ object Automation {
rank += stats.production
rank += stats.science
if (city.tiles.size < 12 || city.civInfo.victoryType() == VictoryType.Cultural) {
if (city.tiles.size < 12 || city.civInfo.wantsToFocusOn(ThingToFocus.Culture)) {
rank += stats.culture
} else rank += stats.culture / 2
}
@ -175,7 +176,7 @@ object Automation {
return true
// Spaceships are always allowed
if (construction.hasUnique(UniqueType.SpaceshipPart))
if (construction.name in civInfo.gameInfo.spaceResources)
return true
val requiredResources = construction.getResourceRequirements()
@ -237,7 +238,7 @@ object Automation {
}
fun getReservedSpaceResourceAmount(civInfo: CivilizationInfo): Int {
return if (civInfo.nation.preferredVictoryType == VictoryType.Scientific) 3 else 2
return if (civInfo.wantsToFocusOn(ThingToFocus.Science)) 3 else 2
}
fun threatAssessment(assessor: CivilizationInfo, assessed: CivilizationInfo): ThreatLevel {

View File

@ -5,7 +5,7 @@ import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.ThingToFocus
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import kotlin.math.min
@ -133,7 +133,7 @@ object ChooseBeliefsAutomation {
score += modifier * when (unique.placeholderText) {
"Earn []% of [] unit's [] as [] when killed within 4 tiles of a city following this religion" ->
unique.params[0].toFloat() * 4f *
if (civInfo.victoryType() == VictoryType.Domination) 2f
if (civInfo.wantsToFocusOn(ThingToFocus.Military)) 2f
else 1f
"May buy [] buildings for [] [] []", "May buy [] units for [] [] []" ->
if (civInfo.religionManager.religion != null
@ -152,7 +152,7 @@ object ChooseBeliefsAutomation {
// what happens over there
else civInfo.statsForNextTurn[Stat.valueOf(unique.params[1])] * 10f / civInfo.getEra().baseUnitBuyCost
UniqueType.BuyUnitsByProductionCost.placeholderText ->
15f * if (civInfo.victoryType() == VictoryType.Domination) 2f else 1f
15f * if (civInfo.wantsToFocusOn(ThingToFocus.Military)) 2f else 1f
"when a city adopts this religion for the first time (modified by game speed)" -> // Modified by personality
unique.stats.values.sum() * 10f
"When spreading religion to a city, gain [] times the amount of followers of other religions as []" ->

View File

@ -8,7 +8,8 @@ import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.BFS
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.ThingToFocus
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
@ -36,8 +37,14 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val allTechsAreResearched = civInfo.tech.getNumberOfTechsResearched() >= civInfo.gameInfo.ruleSet.technologies.size
val isAtWar = civInfo.isAtWar()
val preferredVictoryType = civInfo.victoryType()
private val buildingsForVictory = civInfo.gameInfo.getEnabledVictories().values
.mapNotNull { civInfo.victoryManager.getNextMilestone(it.name) }
.filter { it.type == MilestoneType.BuiltBuilding || it.type == MilestoneType.BuildingBuiltGlobally }
.map { it.params[0] }
private val spaceshipParts = civInfo.gameInfo.spaceResources
private val averageProduction = civInfo.cities.map { it.cityStats.currentCityStats.production }.average()
val cityIsOverAverageProduction = cityInfo.cityStats.currentCityStats.production >= averageProduction
@ -45,9 +52,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private val faithConstruction = arrayListOf<BaseUnit>()
data class ConstructionChoice(val choice:String, var choiceModifier:Float,val remainingWork:Int)
data class ConstructionChoice(val choice: String, var choiceModifier: Float, val remainingWork: Int)
fun addChoice(choices:ArrayList<ConstructionChoice>, choice:String, choiceModifier: Float){
private fun addChoice(choices: ArrayList<ConstructionChoice>, choice: String, choiceModifier: Float){
choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice)))
}
@ -125,7 +132,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1)
// most buildings and civ units contribute the the civ's growth, military units are anti-growth
var modifier = sqrt(unitsToCitiesRatio) / 2
if (preferredVictoryType == VictoryType.Domination) modifier *= 3
if (civInfo.wantsToFocusOn(ThingToFocus.Military)) modifier *= 3
else if (isAtWar) modifier *= unitsToCitiesRatio * 2
if (Automation.afraidOfBarbarians(civInfo)) modifier = 2f // military units are pro-growth if pressured by barbs
@ -193,16 +200,15 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
var modifier = 0.5f
if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it
modifier = 0.8f
if (preferredVictoryType == VictoryType.Cultural) modifier = 1.6f
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)) modifier = 1.6f
addChoice(relativeCostEffectiveness, cultureBuilding.name, modifier)
}
}
private fun addSpaceshipPartChoice() {
val spaceshipPart = (buildableNotWonders + buildableUnits).firstOrNull { it.hasUnique(UniqueType.SpaceshipPart) }
val spaceshipPart = (buildableNotWonders + buildableUnits).firstOrNull { it.name in spaceshipParts }
if (spaceshipPart != null) {
var modifier = 1.5f
if (preferredVictoryType == VictoryType.Scientific) modifier = 2f
val modifier = 2f
addChoice(relativeCostEffectiveness, spaceshipPart.name, modifier)
}
}
@ -217,23 +223,27 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun getWonderPriority(wonder: Building): Float {
if (wonder.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts))
return 2f
if (preferredVictoryType == VictoryType.Cultural
// Only start building if we are the city that would complete it the soonest
if (wonder.hasUnique(UniqueType.TriggersCulturalVictory)
&& cityInfo == civInfo.cities.minByOrNull {
it.cityConstructions.turnsToConstruction(wonder.name)
}!!
) {
return 10f
}
if (wonder.name in buildingsForVictory)
return 5f
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)
// TODO: Moddability
&& wonder.name in listOf("Sistine Chapel", "Eiffel Tower", "Cristo Redentor", "Neuschwanstein", "Sydney Opera House"))
return 3f
// Only start building if we are the city that would complete it the soonest
if (wonder.hasUnique(UniqueType.TriggersCulturalVictory) && cityInfo == civInfo.cities.minByOrNull {
it.cityConstructions.turnsToConstruction(wonder.name)
}!!)
return 10f
if (wonder.isStatRelated(Stat.Science)) {
if (allTechsAreResearched) return .5f
if (preferredVictoryType == VictoryType.Scientific) return 1.5f
if (civInfo.wantsToFocusOn(ThingToFocus.Science)) return 1.5f
else return 1.3f
}
if (wonder.name == "Manhattan Project") {
if (preferredVictoryType == VictoryType.Domination) return 2f
if (civInfo.wantsToFocusOn(ThingToFocus.Military)) return 2f
else return 1.3f
}
if (wonder.isStatRelated(Stat.Happiness)) return 1.2f
@ -259,10 +269,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val unitTrainingBuilding = buildableNotWonders.asSequence()
.filter { it.hasUnique(UniqueType.UnitStartingExperience)
&& Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost }
if (unitTrainingBuilding != null && (preferredVictoryType != VictoryType.Cultural || isAtWar)) {
if (unitTrainingBuilding != null && (!civInfo.wantsToFocusOn(ThingToFocus.Culture) || isAtWar)) {
var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon
if (isAtWar) modifier *= 2
if (preferredVictoryType == VictoryType.Domination)
if (civInfo.wantsToFocusOn(ThingToFocus.Military))
modifier *= 1.3f
addChoice(relativeCostEffectiveness, unitTrainingBuilding.name, modifier)
}
@ -272,7 +282,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val defensiveBuilding = buildableNotWonders.asSequence()
.filter { it.cityStrength > 0
&& Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost }
if (defensiveBuilding != null && (isAtWar || preferredVictoryType != VictoryType.Cultural)) {
if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(ThingToFocus.Culture))) {
var modifier = 0.2f
if (isAtWar) modifier = 0.5f
@ -308,7 +318,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
.minByOrNull { it.cost }
if (scienceBuilding != null) {
var modifier = 1.1f
if (preferredVictoryType == VictoryType.Scientific)
if (civInfo.wantsToFocusOn(ThingToFocus.Science))
modifier *= 1.4f
addChoice(relativeCostEffectiveness, scienceBuilding.name, modifier)
}
@ -352,7 +362,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
// these 4 if conditions are used to determine if an AI should buy units to spread religion, or spend faith to buy things like new military units or new buildings.
// currently this AI can only buy inquisitors and missionaries with faith
// this system will have to be reengineered to support buying other stuff with faith
if (preferredVictoryType == VictoryType.Domination) return
if (civInfo.wantsToFocusOn(ThingToFocus.Military)) return
if (civInfo.religionManager.religion?.name == null) return
if (cityInfo.religion.getMajorityReligion()?.name != civInfo.religionManager.religion?.name)
return // you don't want to build units of opposing religions.
@ -372,7 +382,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
&& it.canBePurchasedWithStat(cityInfo, Stat.Faith) }
if (preferredVictoryType == VictoryType.Cultural) modifier += 1
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)) modifier += 1
if (isAtWar) modifier -= 0.5f
val citiesNotFollowingOurReligion = civInfo.cities.asSequence()

View File

@ -260,7 +260,7 @@ object NextTurnAutomation {
}
}
if (civInfo.victoryType() == VictoryType.Cultural) {
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)) {
for (cityState in civInfo.getKnownCivs()
.filter { it.isCityState() && it.cityStateType == CityStateType.Cultured }) {
val diploManager = cityState.getDiplomacyManager(civInfo)
@ -298,20 +298,20 @@ object NextTurnAutomation {
if (!cityState.isAlive() || cityState.cities.isEmpty() || civInfo.cities.isEmpty())
return value
if (civInfo.victoryType() == VictoryType.Cultural && cityState.canGiveStat(Stat.Culture)) {
if (civInfo.wantsToFocusOn(ThingToFocus.Culture) && cityState.canGiveStat(Stat.Culture)) {
value += 10
}
else if (civInfo.victoryType() == VictoryType.Scientific && cityState.canGiveStat(Stat.Science)) {
else if (civInfo.wantsToFocusOn(ThingToFocus.Science) && cityState.canGiveStat(Stat.Science)) {
// In case someone mods this in
value += 10
}
else if (civInfo.victoryType() == VictoryType.Domination) {
else if (civInfo.wantsToFocusOn(ThingToFocus.Military)) {
// Don't ally close city-states, conquer them instead
val distance = getMinDistanceBetweenCities(civInfo, cityState)
if (distance < 20)
value -= (20 - distance) / 4
}
else if (civInfo.victoryType() == VictoryType.Diplomatic) {
else if (civInfo.wantsToFocusOn(ThingToFocus.CityStates)) {
value += 5 // Generally be friendly
}
if (civInfo.gold < 100) {
@ -658,7 +658,7 @@ object NextTurnAutomation {
}
private fun declareWar(civInfo: CivilizationInfo) {
if (civInfo.victoryType() == VictoryType.Cultural) return
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)) return
if (civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
if (civInfo.isAtWar() || civInfo.getHappiness() <= 0) return
@ -848,7 +848,7 @@ object NextTurnAutomation {
private fun trainSettler(civInfo: CivilizationInfo) {
if (civInfo.isCityState()) return
if (civInfo.isAtWar()) return // don't train settlers when you could be training troops.
if (civInfo.victoryType() == VictoryType.Cultural && civInfo.cities.size > 3) return
if (civInfo.wantsToFocusOn(ThingToFocus.Culture) && civInfo.cities.size > 3) return
if (civInfo.cities.none() || civInfo.getHappiness() <= civInfo.cities.size + 5) return
val settlerUnits = civInfo.gameInfo.ruleSet.units.values

View File

@ -1,7 +1,7 @@
package com.unciv.logic.city
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.ruleset.IHasUniques
import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.INamed
@ -20,6 +20,7 @@ interface IConstruction : INamed {
interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
val hurryCostModifier: Int
var requiredTech: String?
fun getProductionCost(civInfo: CivilizationInfo): Int
fun getStatBuyCost(cityInfo: CityInfo, stat: Stat): Int?
@ -111,14 +112,14 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
RejectionReason.RequiresTech,
RejectionReason.RequiresPolicy,
RejectionReason.MorePolicyBranches,
RejectionReason.RequiresBuildingInSomeCity
RejectionReason.RequiresBuildingInSomeCity,
)
private val reasonsToDefinitivelyRemoveFromQueue = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.WonderAlreadyBuilt,
RejectionReason.NationalWonderAlreadyBuilt,
RejectionReason.CannotBeBuiltWith,
RejectionReason.ReachedBuildCap
RejectionReason.MaxNumberBuildable,
)
private val orderOfErrorMessages = listOf(
RejectionReason.WonderBeingBuiltElsewhere,
@ -130,7 +131,7 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
RejectionReason.ConsumesResources,
RejectionReason.CanOnlyBePurchased,
RejectionReason.MaxNumberBuildable,
RejectionReason.NoPlaceToPutUnit
RejectionReason.NoPlaceToPutUnit,
)
}
}
@ -152,7 +153,7 @@ enum class RejectionReason(val shouldShow: Boolean, val errorMessage: String) {
MustOwnTile(false, "Must own a specific tile close by"),
WaterUnitsInCoastalCities(false, "May only built water units in coastal cities"),
CanOnlyBeBuiltInSpecificCities(false, "Can only be built in specific cities"),
MaxNumberBuildable(true, "Maximum number have been built or are being constructed"),
MaxNumberBuildable(false, "Maximum number have been built or are being constructed"),
UniqueToOtherNation(false, "Unique to another nation"),
ReplacedByOurUnique(false, "Our unique replaces this"),
@ -179,8 +180,6 @@ enum class RejectionReason(val shouldShow: Boolean, val errorMessage: String) {
CityStateNationalWonder(false, "No National Wonders for city-states"),
WonderDisabledEra(false, "This Wonder is disabled when starting in this era"),
ReachedBuildCap(false, "Don't need to build any more of these!"),
ConsumesResources(true, "Consumes resources which you are lacking"),
PopulationRequirement(true, "Requires more population"),

View File

@ -1,13 +1,13 @@
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
import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.automation.WorkerAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.IConstruction
import com.unciv.logic.civilization.RuinsManager.RuinsManager
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
@ -119,6 +119,9 @@ class CivilizationInfo {
@Transient
val lastEraResourceUsedForUnit = HashMap<String, Int>()
@Transient
var thingsToFocusOnForVictory = setOf<ThingToFocus>()
var playerType = PlayerType.AI
@ -322,21 +325,33 @@ class CivilizationInfo {
fun isAlive(): Boolean = !isDefeated()
@Suppress("unused") //TODO remove if future use unlikely, including DiplomacyFlags.EverBeenFriends and 2 DiplomacyManager methods - see #3183
// I'm willing to call this deprecated after so long
fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends()
fun hasMetCivTerritory(otherCiv: CivilizationInfo): Boolean = otherCiv.getCivTerritory().any { it in exploredTiles }
fun getCompletedPolicyBranchesCount(): Int = policies.adoptedPolicies.count { Policy.isBranchCompleteByName(it) }
fun originalMajorCapitalsOwned(): Int = cities.count { it.isOriginalCapital && it.foundingCiv != "" && gameInfo.getCivilization(it.foundingCiv).isMajorCiv() }
private fun getCivTerritory() = cities.asSequence().flatMap { it.tiles.asSequence() }
fun victoryType(): VictoryType {
fun getPreferredVictoryType(): String {
val victoryTypes = gameInfo.gameParameters.victoryTypes
if (victoryTypes.size == 1)
return victoryTypes.first() // That is the most relevant one
val victoryType = nation.preferredVictoryType
return if (victoryType in victoryTypes) victoryType
else VictoryType.Neutral
return if (victoryType in gameInfo.ruleSet.victories) victoryType
else Constants.neutralVictoryType
}
fun getPreferredVictoryTypeObject(): Victory? {
val preferredVictoryType = getPreferredVictoryType()
return if (preferredVictoryType == Constants.neutralVictoryType) null
else gameInfo.ruleSet.victories[getPreferredVictoryType()]!!
}
fun wantsToFocusOn(thingToFocusOn: ThingToFocus): Boolean {
return thingsToFocusOnForVictory.contains(thingToFocusOn)
}
@Transient
private val civInfoStats = CivInfoStats(this)
fun stats() = civInfoStats
@ -751,6 +766,8 @@ class CivilizationInfo {
}
victoryManager.civInfo = this
thingsToFocusOnForVictory = getPreferredVictoryTypeObject()?.getThingsToFocus(this) ?: setOf()
for (cityInfo in cities) {
cityInfo.civInfo = this // must be before the city's setTransients because it depends on the tilemap, that comes from the currentPlayerCivInfo
@ -928,16 +945,17 @@ class CivilizationInfo {
private fun handleDiplomaticVictoryFlags() {
if (flagsCountdown[CivFlags.ShouldResetDiplomaticVotes.name] == 0) {
gameInfo.diplomaticVictoryVotesCast.clear()
removeFlag(CivFlags.ShouldResetDiplomaticVotes.name)
removeFlag(CivFlags.ShowDiplomaticVotingResults.name)
removeFlag(CivFlags.ShouldResetDiplomaticVotes.name)
}
if (flagsCountdown[CivFlags.ShowDiplomaticVotingResults.name] == 0) {
gameInfo.processDiplomaticVictory()
if (gameInfo.civilizations.any { it.victoryManager.hasWon() } ) {
removeFlag(CivFlags.TurnsTillNextDiplomaticVote.name)
} else {
addFlag(CivFlags.ShouldResetDiplomaticVotes.name, 1)
addFlag(CivFlags.TurnsTillNextDiplomaticVote.name, getTurnsBetweenDiplomaticVotings())
addFlag(CivFlags.TurnsTillNextDiplomaticVote.name, getTurnsBetweenDiplomaticVotes())
}
}
@ -950,7 +968,7 @@ class CivilizationInfo {
fun removeFlag(flag: String) = flagsCountdown.remove(flag)
fun hasFlag(flag: String) = flagsCountdown.contains(flag)
fun getTurnsBetweenDiplomaticVotings() = (15 * gameInfo.gameParameters.gameSpeed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files
fun getTurnsBetweenDiplomaticVotes() = (15 * gameInfo.gameParameters.gameSpeed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files
fun getTurnsTillNextDiplomaticVote() = flagsCountdown[CivFlags.TurnsTillNextDiplomaticVote.name]
@ -975,8 +993,8 @@ class CivilizationInfo {
// to the user and thus the flag is set at -1/
fun shouldCheckForDiplomaticVictory() =
(flagsCountdown[CivFlags.ShowDiplomaticVotingResults.name] == 0
|| flagsCountdown[CivFlags.ShowDiplomaticVotingResults.name] == -1)
&& gameInfo.civilizations.any { it.isMajorCiv() && !it.isDefeated() && it != this }
|| flagsCountdown[CivFlags.ShowDiplomaticVotingResults.name] == -1)
&& gameInfo.civilizations.any { it.isMajorCiv() && !it.isDefeated() && it != this }
private fun updateRevolts() {
if (gameInfo.civilizations.none { it.isBarbarian() }) {

View File

@ -38,7 +38,7 @@ class PolicyManager {
get() {
val value = HashMap<PolicyBranch, Int>()
for (branch in branches) {
value[branch] = branch.priorities[civInfo.victoryType().name] ?: 0
value[branch] = branch.priorities[civInfo.getPreferredVictoryType()] ?: 0
}
return value
}

View File

@ -1,39 +1,27 @@
package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.models.Counter
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.Milestone
import com.unciv.models.ruleset.unique.UniqueType
class VictoryManager {
@Transient
lateinit var civInfo: CivilizationInfo
var requiredSpaceshipParts = Counter<String>()
// There is very likely a typo in this name (currents), but as its saved in save files,
// fixing it is non-trivial
var currentsSpaceshipParts = Counter<String>()
var hasWonTimeVictory = false
init {
requiredSpaceshipParts.add("SS Booster", 3)
requiredSpaceshipParts.add("SS Cockpit", 1)
requiredSpaceshipParts.add("SS Engine", 1)
requiredSpaceshipParts.add("SS Stasis Chamber", 1)
}
var hasEverWonDiplomaticVote = false
fun clone(): VictoryManager {
val toReturn = VictoryManager()
toReturn.currentsSpaceshipParts.putAll(currentsSpaceshipParts)
toReturn.hasEverWonDiplomaticVote = hasEverWonDiplomaticVote
return toReturn
}
fun unconstructedSpaceshipParts(): Counter<String> {
val counter = requiredSpaceshipParts.clone()
counter.remove(currentsSpaceshipParts)
return counter
}
fun spaceshipPartsRemaining() = requiredSpaceshipParts.values.sum() - currentsSpaceshipParts.values.sum()
fun calculateDiplomaticVotingResults(votesCast: HashMap<String, String>): Counter<String> {
private fun calculateDiplomaticVotingResults(votesCast: HashMap<String, String>): Counter<String> {
val results = Counter<String>()
for (castVote in votesCast) {
results.add(castVote.value, 1)
@ -41,7 +29,7 @@ class VictoryManager {
return results
}
fun votesNeededForDiplomaticVictory(): Int {
private fun votesNeededForDiplomaticVictory(): Int {
val civCount = civInfo.gameInfo.civilizations.count { !it.isDefeated() }
// CvGame.cpp::DoUpdateDiploVictory() in the source code of the original
@ -54,44 +42,46 @@ class VictoryManager {
fun hasEnoughVotesForDiplomaticVictory(): Boolean {
val results = calculateDiplomaticVotingResults(civInfo.gameInfo.diplomaticVictoryVotesCast)
val bestCiv = results.maxByOrNull { it.value } ?: return false
// If we don't have the highest score, we have not won anyway
if (bestCiv.key != civInfo.civName) return false
// If we don't have enough votes, we haven't won
if (bestCiv.value < votesNeededForDiplomaticVictory()) return false
// If there's a tie, we haven't won either
return (results.none { it != bestCiv && it.value == bestCiv.value })
}
private fun hasVictoryType(victoryType: VictoryType) = civInfo.gameInfo.gameParameters.victoryTypes.contains(victoryType)
fun hasWonScientificVictory() = hasVictoryType(VictoryType.Scientific)
&& spaceshipPartsRemaining() == 0
fun hasWonCulturalVictory() = hasVictoryType(VictoryType.Cultural)
&& civInfo.hasUnique(UniqueType.TriggersCulturalVictory)
fun hasWonDominationVictory()= hasVictoryType(VictoryType.Domination)
&& civInfo.gameInfo.civilizations.all { it == civInfo || it.isDefeated() || !it.isMajorCiv() }
fun hasWonDiplomaticVictory() = hasVictoryType(VictoryType.Diplomatic)
&& civInfo.shouldCheckForDiplomaticVictory()
&& hasEnoughVotesForDiplomaticVictory()
fun hasWonTimeVictory() = hasVictoryType(VictoryType.Time)
&& hasWonTimeVictory
fun hasWonVictoryType(): VictoryType? {
fun getVictoryTypeAchieved(): String? {
if (!civInfo.isMajorCiv()) return null
if (hasWonTimeVictory()) return VictoryType.Time
if (hasWonDominationVictory()) return VictoryType.Domination
if (hasWonScientificVictory()) return VictoryType.Scientific
if (hasWonCulturalVictory()) return VictoryType.Cultural
if (hasWonDiplomaticVictory()) return VictoryType.Diplomatic
if (civInfo.hasUnique(UniqueType.TriggersVictory)) return VictoryType.Neutral
for (victoryName in civInfo.gameInfo.ruleSet.victories.keys.filter { it != Constants.neutralVictoryType}) {
if (getNextMilestone(victoryName) == null)
return victoryName
}
if (civInfo.hasUnique(UniqueType.TriggersVictory))
return Constants.neutralVictoryType
return null
}
fun getNextMilestone(victory: String): Milestone? {
for (milestone in civInfo.gameInfo.ruleSet.victories[victory]!!.milestoneObjects) {
if (!milestone.hasBeenCompletedBy(civInfo))
return milestone
}
return null
}
fun amountMilestonesCompleted(victory: String): Int {
var completed = 0
for (milestone in civInfo.gameInfo.ruleSet.victories[victory]!!.milestoneObjects) {
if (milestone.hasBeenCompletedBy(civInfo))
++completed
else
break
}
return completed
}
fun hasWon() = hasWonVictoryType() != null
fun hasWon() = getVictoryTypeAchieved() != null
}

View File

@ -34,6 +34,10 @@ open class Counter<K> : LinkedHashMap<K, Int>() {
for (key in keys) newCounter[key] = this[key]!! * amount
return newCounter
}
fun sumValues(): Int {
return this.map { it.value }.sum()
}
override fun clone(): Counter<K> {
val newCounter = Counter<K>()

View File

@ -1,10 +1,6 @@
package com.unciv.models.metadata
import com.unciv.Constants
import com.unciv.logic.civilization.PlayerType
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.VictoryType
enum class BaseRuleset(val fullName:String){
Civ_V_Vanilla("Civ V - Vanilla"),
@ -26,9 +22,8 @@ class GameParameters { // Default values are the default new game
var godMode = false
var nuclearWeaponsEnabled = true
var religionEnabled = false
// By default, all victory types except Diplomacy and time as they are quite new
var victoryTypes: ArrayList<VictoryType> = arrayListOf(VictoryType.Cultural, VictoryType.Domination, VictoryType.Scientific)
var victoryTypes: ArrayList<String> = arrayListOf()
var startingEra = "Ancient era"
var isOnlineMultiplayer = false
@ -70,9 +65,7 @@ class GameParameters { // Default values are the default new game
if (!nuclearWeaponsEnabled) yield("No nukes")
if (religionEnabled) yield("Religion")
if (godMode) yield("God mode")
for (victoryType in VictoryType.values()) {
if (victoryType !in victoryTypes) yield("No $victoryType Victory")
}
yield("Enabled Victories: " + victoryTypes.joinToString())
yield(baseRuleset)
yield(if (mods.isEmpty()) "no mods" else mods.joinToString(",", "mods=(", ")", 6) )
}.joinToString(prefix = "(", postfix = ")")

View File

@ -18,7 +18,7 @@ import kotlin.math.pow
class Building : RulesetStatsObject(), INonPerpetualConstruction {
var requiredTech: String? = null
override var requiredTech: String? = null
var cost: Int = 0
var maintenance = 0
@ -535,11 +535,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
}
// To be replaced with `Only available <after [Apollo Project] has been build>`
UniqueType.SpaceshipPart -> {
if (!civInfo.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts))
rejectionReasons.add(RejectionReason.RequiresBuildingInSomeCity.toInstance("Apollo project not built!"))
if (civInfo.victoryManager.unconstructedSpaceshipParts()[name] == 0)
rejectionReasons.add(RejectionReason.ReachedBuildCap)
}
UniqueType.RequiresAnotherBuilding -> {
@ -590,7 +589,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
UniqueType.HiddenWithoutVictoryType -> {
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(unique.params[0])))
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(unique.params[0]))
rejectionReasons.add(RejectionReason.HiddenWithoutVictory.toInstance(unique.text))
}
}
@ -670,7 +669,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean {
val civInfo = cityConstructions.cityInfo.civInfo
if (hasUnique(UniqueType.SpaceshipPart)) {
if (civInfo.gameInfo.spaceResources.contains(name)) {
civInfo.victoryManager.currentsSpaceshipParts.add(name, 1)
return true
}

View File

@ -3,9 +3,9 @@ package com.unciv.models.ruleset
import com.badlogic.gdx.graphics.Color
import com.unciv.logic.civilization.CityStateType
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.stats.INamed
import com.unciv.ui.utils.colorFromRGB
class Era : RulesetObject(), IHasUniques {

View File

@ -14,15 +14,6 @@ import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.utils.Fonts
import com.unciv.ui.utils.colorFromRGB
enum class VictoryType {
Neutral,
Cultural,
Diplomatic,
Domination,
Scientific,
Time,
}
class Nation : RulesetObject() {
var leaderName = ""
fun getLeaderDisplayName() = if (isCityState()) name
@ -30,7 +21,7 @@ class Nation : RulesetObject() {
val style = ""
var cityStateType: CityStateType? = null
var preferredVictoryType: VictoryType = VictoryType.Neutral
var preferredVictoryType: String = Constants.neutralVictoryType
var declaringWar = ""
var attacked = ""
var defeated = ""

View File

@ -16,6 +16,7 @@ import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
@ -92,6 +93,7 @@ class Ruleset {
val units = LinkedHashMap<String, BaseUnit>()
val unitPromotions = LinkedHashMap<String, Promotion>()
val unitTypes = LinkedHashMap<String, UnitType>()
var victories = LinkedHashMap<String, Victory>()
val mods = LinkedHashSet<String>()
var modOptions = ModOptions()
@ -132,6 +134,7 @@ class Ruleset {
unitPromotions.putAll(ruleset.unitPromotions)
units.putAll(ruleset.units)
unitTypes.putAll(ruleset.unitTypes)
victories.putAll(ruleset.victories)
for (unitToRemove in ruleset.modOptions.unitsToRemove) units.remove(unitToRemove)
modOptions.uniques.addAll(ruleset.modOptions.uniques)
modOptions.constants.merge(ruleset.modOptions.constants)
@ -159,6 +162,7 @@ class Ruleset {
unitPromotions.clear()
units.clear()
unitTypes.clear()
victories.clear()
}
fun allRulesetObjects(): Sequence<IRulesetObject> =
@ -181,6 +185,7 @@ class Ruleset {
unitPromotions.values.asSequence() +
units.values.asSequence() +
unitTypes.values.asSequence()
// Victories is only INamed
fun allIHasUniques(): Sequence<IHasUniques> =
allRulesetObjects() + sequenceOf(modOptions)
@ -258,7 +263,7 @@ class Ruleset {
// Setup this branch
branch.requires = ArrayList()
branch.branch = branch
for (victoryType in VictoryType.values()) {
for (victoryType in victories.values) {
if (victoryType.name !in branch.priorities.keys) {
branch.priorities[victoryType.name] = 0
}
@ -306,6 +311,33 @@ class Ruleset {
if (globalUniquesFile.exists()) {
globalUniques = jsonParser.getFromJson(GlobalUniques::class.java, globalUniquesFile)
}
val victoryTypesFiles = folderHandle.child("VictoryTypes.json")
if (victoryTypesFiles.exists()) {
victories += createHashmap(jsonParser.getFromJson(Array<Victory>::class.java, victoryTypesFiles))
}
// Add objects that might not be present in base ruleset mods, but are required
if (modOptions.isBaseRuleset) {
// This one should be temporary
if (unitTypes.isEmpty()) {
unitTypes.putAll(RulesetCache.getVanillaRuleset().unitTypes)
}
// These should be permanent
if (ruinRewards.isEmpty()) {
ruinRewards.putAll(RulesetCache.getVanillaRuleset().ruinRewards)
}
if (globalUniques.uniques.isEmpty()) {
globalUniques = RulesetCache.getVanillaRuleset().globalUniques
}
// If we have no victories, add all the default victories
if (victories.isEmpty()) {
victories.putAll(RulesetCache.getVanillaRuleset().victories)
}
}
val gameBasicsLoadTime = System.currentTimeMillis() - gameBasicsStartTime
if (printOutput) println("Loading ruleset - " + gameBasicsLoadTime + "ms")
@ -354,7 +386,7 @@ class Ruleset {
forOptionsPopup: Boolean
) {
val name = if (uniqueContainer is INamed) uniqueContainer.name else ""
for (unique in uniqueContainer.uniqueObjects) {
val errors = checkUnique(
unique,
@ -771,6 +803,18 @@ class Ruleset {
for (unitType in unitTypes.values) {
checkUniques(unitType, lines, rulesetSpecific, forOptionsPopup)
}
for (victoryType in victories.values) {
for (requiredUnit in victoryType.requiredSpaceshipParts)
if (!units.contains(requiredUnit))
lines.add("Victory type ${victoryType.name} requires adding the non-existant unit $requiredUnit to the capital to win!", RulesetErrorSeverity.Warning)
for (milestone in victoryType.milestoneObjects)
if (milestone.type == null)
lines.add("Victory type ${victoryType.name} has milestone ${milestone.uniqueDescription} that is of an unknown type!", RulesetErrorSeverity.Error)
for (victory in victories.values)
if (victory.name != victoryType.name && victory.milestones == victoryType.milestones)
lines.add("Victory types ${victoryType.name} and ${victory.name} have the same requirements!", RulesetErrorSeverity.Warning)
}
for (difficulty in difficulties.values) {
for (unitName in difficulty.aiCityStateBonusStartingUnits + difficulty.aiMajorCivBonusStartingUnits + difficulty.playerBonusStartingUnits)
@ -878,7 +922,6 @@ object RulesetCache : HashMap<String,Ruleset>() {
this[optionalBaseRuleset]!!
else getVanillaRuleset()
val loadedMods = mods.asSequence()
.filter { containsKey(it) }
.map { this[it]!! }
@ -896,19 +939,9 @@ object RulesetCache : HashMap<String,Ruleset>() {
}
newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs
// This one should be temporary
if (newRuleset.unitTypes.isEmpty()) {
newRuleset.unitTypes.putAll(getVanillaRuleset().unitTypes)
}
// These should be permanent
if (newRuleset.ruinRewards.isEmpty()) {
newRuleset.ruinRewards.putAll(getVanillaRuleset().ruinRewards)
}
if (newRuleset.globalUniques.uniques.isEmpty()) {
newRuleset.globalUniques = getVanillaRuleset().globalUniques
}
println(optionalBaseRuleset)
println(newRuleset.victories)
return newRuleset
}

View File

@ -1,12 +1,13 @@
package com.unciv.models.ruleset
import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.stats.INamed
import com.unciv.models.stats.NamedStats
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.ICivilopediaText
interface IRulesetObject:INamed, IHasUniques, ICivilopediaText
interface IRulesetObject: INamed, IHasUniques, ICivilopediaText
abstract class RulesetObject: IRulesetObject {
override var name = ""

View File

@ -0,0 +1,240 @@
package com.unciv.models.ruleset
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.Constants
import com.unciv.models.stats.INamed
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.Counter
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
import com.unciv.ui.utils.toTextButton
enum class MilestoneType(val text: String) {
BuiltBuilding("Build [building]"),
BuildingBuiltGlobally("[building] build globally"),
AddedSSPartsInCapital("Add all [comment] in capital"),
DestroyAllPlayers("Destroy all players"),
CaptureAllCapitals("Capture all capitals"),
CompletePolicyBranches("Complete [amount] Policy branches"),
WinDiplomaticVote("Win diplomatic vote"),
ScoreAfterTimeOut("Have highest score after max turns"),
}
enum class CompletionStatus {
Completed,
Partially,
Incomplete
}
enum class ThingToFocus {
Production,
Gold,
Science,
Culture,
Military,
CityStates,
Score,
}
class Victory : INamed {
override var name = ""
val victoryScreenHeader = "Do things to win!"
val hiddenInVictoryScreen = false
// Things to do to win
// Needs to be ordered, as the milestones are supposed to be obtained in a specific order
val milestones = ArrayList<String>()
val milestoneObjects by lazy { milestones.map { Milestone(it, this) }}
val requiredSpaceshipParts = ArrayList<String>()
val requiredSpaceshipPartsAsCounter by lazy {
val parts = Counter<String>()
for (spaceshipPart in requiredSpaceshipParts)
parts.add(spaceshipPart, 1)
parts
}
val victoryString = "Your civilization stands above all others! The exploits of your people shall be remembered until the end of civilization itself!"
val defeatString = "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
fun enablesMaxTurns(): Boolean = milestoneObjects.any { it.type == MilestoneType.ScoreAfterTimeOut }
fun getThingsToFocus(civInfo: CivilizationInfo): Set<ThingToFocus> = milestoneObjects
.filter { !it.hasBeenCompletedBy(civInfo) }
.map { it.getThingToFocus(civInfo) }
.toSet()
}
class Milestone(val uniqueDescription: String, private val accompaniedVictory: Victory) {
val type: MilestoneType? = MilestoneType.values().firstOrNull { uniqueDescription.getPlaceholderText() == it.text.getPlaceholderText() }
val params by lazy { uniqueDescription.getPlaceholderParameters() }
fun getIncompleteSpaceshipParts(civInfo: CivilizationInfo): Counter<String> {
val incompleteSpaceshipParts = accompaniedVictory.requiredSpaceshipPartsAsCounter.clone()
incompleteSpaceshipParts.remove(civInfo.victoryManager.currentsSpaceshipParts)
return incompleteSpaceshipParts
}
fun hasBeenCompletedBy(civInfo: CivilizationInfo): Boolean {
return when (type!!) {
MilestoneType.BuiltBuilding ->
civInfo.cities.any { it.cityConstructions.builtBuildings.contains(params[0])}
MilestoneType.AddedSSPartsInCapital -> {
getIncompleteSpaceshipParts(civInfo).isEmpty()
}
MilestoneType.DestroyAllPlayers ->
civInfo.gameInfo.getAliveMajorCivs() == listOf(civInfo)
MilestoneType.CaptureAllCapitals ->
civInfo.originalMajorCapitalsOwned() == civInfo.gameInfo.civilizations.count { it.isMajorCiv() }
MilestoneType.CompletePolicyBranches ->
civInfo.policies.completedBranches.size >= params[0].toInt()
MilestoneType.BuildingBuiltGlobally -> civInfo.gameInfo.getCities().any {
it.cityConstructions.builtBuildings.contains(params[0])
}
MilestoneType.WinDiplomaticVote -> civInfo.victoryManager.hasEverWonDiplomaticVote
MilestoneType.ScoreAfterTimeOut -> {
civInfo.gameInfo.turns >= civInfo.gameInfo.gameParameters.maxTurns
&& civInfo == civInfo.gameInfo.civilizations.maxByOrNull { it.calculateTotalScore() }
}
}
}
private fun getMilestoneButton(text: String, achieved: Boolean): TextButton {
val textButton = text.toTextButton()
if (achieved) textButton.color = Color.GREEN
else textButton.color = Color.GRAY
return textButton
}
fun getVictoryScreenButtonHeaderText(completed: Boolean, civInfo: CivilizationInfo): String {
return when (type!!) {
MilestoneType.BuildingBuiltGlobally, MilestoneType.WinDiplomaticVote,
MilestoneType.ScoreAfterTimeOut, MilestoneType.BuiltBuilding ->
uniqueDescription
MilestoneType.CompletePolicyBranches -> {
val amountToDo = params[0]
val amountDone =
if (completed) amountToDo
else civInfo.getCompletedPolicyBranchesCount()
"{$uniqueDescription} ($amountDone/$amountToDo)"
}
MilestoneType.CaptureAllCapitals -> {
val amountToDo = civInfo.gameInfo.civilizations.count { it.isMajorCiv() }
val amountDone =
if (completed) amountToDo
else civInfo.originalMajorCapitalsOwned()
"{$uniqueDescription} ($amountDone/$amountToDo)"
}
MilestoneType.DestroyAllPlayers -> {
val amountToDo = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't count yourself
val amountDone =
if (completed) amountToDo
else amountToDo - (civInfo.gameInfo.getAliveMajorCivs().filter { it != civInfo }.count())
"{$uniqueDescription} ($amountDone/$amountToDo)"
}
MilestoneType.AddedSSPartsInCapital -> {
val completeSpaceshipParts = civInfo.victoryManager.currentsSpaceshipParts
val incompleteSpaceshipParts = accompaniedVictory.requiredSpaceshipPartsAsCounter.clone()
val amountToDo = incompleteSpaceshipParts.sumValues()
incompleteSpaceshipParts.remove(completeSpaceshipParts)
val amountDone = amountToDo - incompleteSpaceshipParts.sumValues()
"{$uniqueDescription} ($amountDone/$amountToDo)"
}
}
}
fun getVictoryScreenButtons(completionStatus: CompletionStatus, civInfo: CivilizationInfo): List<TextButton> {
val headerButton = getMilestoneButton(
getVictoryScreenButtonHeaderText(completionStatus == CompletionStatus.Completed, civInfo),
completionStatus == CompletionStatus.Completed
)
if (completionStatus == CompletionStatus.Completed || completionStatus == CompletionStatus.Incomplete) {
// When done or not working on this milestone, only show the header button
return listOf(headerButton)
}
// Otherwise, append the partial buttons of each step
val buttons = mutableListOf(headerButton)
when (type) {
// No extra buttons necessary
MilestoneType.BuiltBuilding, MilestoneType.BuildingBuiltGlobally,
MilestoneType.ScoreAfterTimeOut, MilestoneType.WinDiplomaticVote -> {}
MilestoneType.AddedSSPartsInCapital -> {
val completedSpaceshipParts = civInfo.victoryManager.currentsSpaceshipParts
val incompleteSpaceshipParts = getIncompleteSpaceshipParts(civInfo)
for (part in completedSpaceshipParts) {
repeat(part.value) {
buttons.add(getMilestoneButton(part.key, true))
}
}
for (part in incompleteSpaceshipParts) {
repeat(part.value) {
buttons.add(getMilestoneButton(part.key, false))
}
}
}
MilestoneType.DestroyAllPlayers -> {
for (civ in civInfo.gameInfo.civilizations.filter { it != civInfo && it.isMajorCiv() && !it.isAlive() }) {
buttons.add(getMilestoneButton("Destroy [${civ.civName}]", true))
}
for (civ in civInfo.gameInfo.getAliveMajorCivs().filter { it != civInfo}) {
buttons.add(getMilestoneButton("Destroy [${civ.civName}]", false))
}
}
MilestoneType.CaptureAllCapitals -> {
for (city in civInfo.gameInfo.getAliveMajorCivs().mapNotNull {
civ -> civ.cities.firstOrNull { it.isOriginalCapital && it.foundingCiv == civ.civName }
}
) {
buttons.add(getMilestoneButton("Capture [${city.name}]", false))
}
}
MilestoneType.CompletePolicyBranches -> {
for (branch in civInfo.gameInfo.ruleSet.policyBranches.values) {
val finisher = branch.policies.last().name
buttons.add(getMilestoneButton(finisher, civInfo.policies.isAdopted(finisher)))
}
}
}
return buttons
}
fun getThingToFocus(civInfo: CivilizationInfo): ThingToFocus {
val ruleset = civInfo.gameInfo.ruleSet
return when (type!!) {
MilestoneType.BuiltBuilding -> {
val building = ruleset.buildings[params[0]]!!
if (building.requiredTech != null && !civInfo.tech.isResearched(building.requiredTech!!)) ThingToFocus.Science
// if (building.hasUnique(UniqueType.Unbuildable)) Stat.Gold // Temporary, should be replaced with whatever is required to buy
ThingToFocus.Production
}
MilestoneType.BuildingBuiltGlobally -> {
val building = ruleset.buildings[params[0]]!!
if (building.requiredTech != null && !civInfo.tech.isResearched(building.requiredTech!!)) ThingToFocus.Science
// if (building.hasUnique(UniqueType.Unbuildable)) ThingToFocus.Gold
ThingToFocus.Production
}
MilestoneType.AddedSSPartsInCapital -> {
val constructions =
getIncompleteSpaceshipParts(civInfo).keys.map {
if (it in ruleset.buildings)
ruleset.buildings[it]!!
else ruleset.units[it]!!
}
if (constructions.any { it.requiredTech != null && !civInfo.tech.isResearched(it.requiredTech!!) } ) ThingToFocus.Science
// if (constructions.any { it.hasUnique(UniqueType.Unbuildable) } ) Stat.Gold
ThingToFocus.Production
}
MilestoneType.DestroyAllPlayers, MilestoneType.CaptureAllCapitals -> ThingToFocus.Military
MilestoneType.CompletePolicyBranches -> ThingToFocus.Culture
MilestoneType.WinDiplomaticVote -> ThingToFocus.CityStates
MilestoneType.ScoreAfterTimeOut -> ThingToFocus.Score
}
}
}

View File

@ -1,9 +1,4 @@
package com.unciv.models.ruleset
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
package com.unciv.models.ruleset.unique
/**
* Common interface for all 'ruleset objects' that have Uniques, like BaseUnit, Nation, etc.

View File

@ -5,7 +5,6 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.TranslationFileWriter // for Kdoc only
@ -429,7 +428,7 @@ enum class UniqueParameterType(
parameterText: String,
ruleset: Ruleset
): UniqueType.UniqueComplianceErrorSeverity? {
return if (parameterText in VictoryType.values().map { it.name }) null
return if (parameterText in ruleset.victories) null
else UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant
}
},

View File

@ -6,8 +6,8 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.*
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.ThingToFocus
import com.unciv.models.ruleset.unique.UniqueType.*
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.hasPlaceholderParameters
@ -144,14 +144,12 @@ object UniqueTriggerActivation {
if (greatPeople.isEmpty()) return false
var greatPerson = greatPeople.random()
val preferredVictoryType = civInfo.victoryType()
if (preferredVictoryType == VictoryType.Cultural) {
if (civInfo.wantsToFocusOn(ThingToFocus.Culture)) {
val culturalGP =
greatPeople.firstOrNull { it.uniques.contains("Great Person - [Culture]") }
if (culturalGP != null) greatPerson = culturalGP
}
if (preferredVictoryType == VictoryType.Scientific) {
if (civInfo.wantsToFocusOn(ThingToFocus.Science)) {
val scientificGP =
greatPeople.firstOrNull { it.uniques.contains("Great Person - [Science]") }
if (scientificGP != null) greatPerson = scientificGP
@ -459,7 +457,7 @@ object UniqueTriggerActivation {
if (!civ.isBarbarian() && !civ.isSpectator())
civ.addFlag(
CivFlags.TurnsTillNextDiplomaticVote.name,
civInfo.getTurnsBetweenDiplomaticVotings()
civInfo.getTurnsBetweenDiplomaticVotes()
)
if (notification != null)
civInfo.addNotification(notification, NotificationIcon.Diplomacy)

View File

@ -496,7 +496,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
CannotBeBarbarian("Never appears as a Barbarian unit", UniqueTarget.Unit, flags = UniqueFlag.setOfHiddenToUsers),
ReligiousUnit("Religious Unit", UniqueTarget.Unit),
SpaceshipPart("Spaceship part", UniqueTarget.Unit, UniqueTarget.Building), // Usage for buildings is deprecated
SpaceshipPart("Spaceship part", UniqueTarget.Unit, UniqueTarget.Building), // Should be deprecated in the near future
AddInCapital("Can be added to [comment] in the Capital", UniqueTarget.Unit),

View File

@ -35,7 +35,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
var interceptRange = 0
lateinit var unitType: String
fun getType() = ruleset.unitTypes[unitType]!!
var requiredTech: String? = null
override var requiredTech: String? = null
private var requiredResource: String? = null
override fun getUniqueTarget() = UniqueTarget.Unit

View File

@ -1,10 +1,7 @@
package com.unciv.models.ruleset.unit
import com.unciv.models.ruleset.IHasUniques
import com.unciv.models.ruleset.RulesetObject
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.stats.INamed
enum class UnitLayer { // The layer in which the unit moves

View File

@ -1,9 +1,9 @@
package com.unciv.models.simulation
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.GameStarter
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.crashhandling.crashHandlingThread
import kotlin.time.Duration
@ -23,7 +23,7 @@ class Simulation(
private var endTime: Long = 0
var steps = ArrayList<SimulationStep>()
var winRate = mutableMapOf<String, MutableInt>()
private var winRateByVictory = HashMap<String, MutableMap<VictoryType, MutableInt>>()
private var winRateByVictory = HashMap<String, MutableMap<String, MutableInt>>()
private var avgSpeed = 0f
private var avgDuration: Duration = Duration.ZERO
private var totalTurns = 0
@ -35,7 +35,7 @@ class Simulation(
for (civ in civilizations) {
this.winRate[civ] = MutableInt(0)
winRateByVictory[civ] = mutableMapOf()
for (victory in VictoryType.values())
for (victory in UncivGame.Current.gameInfo.ruleSet.victories.keys)
winRateByVictory[civ]!![victory] = MutableInt(0)
}
}
@ -118,7 +118,7 @@ class Simulation(
outString += "\n$civ:\n"
val wins = winRate[civ]!!.value * 100 / max(steps.size, 1)
outString += "$wins% total win rate \n"
for (victory in VictoryType.values()) {
for (victory in UncivGame.Current.gameInfo.ruleSet.victories.keys) {
val winsVictory = winRateByVictory[civ]!![victory]!!.value * 100 / max(winRate[civ]!!.value, 1)
outString += "$victory: $winsVictory% "
}

View File

@ -4,7 +4,7 @@ import com.unciv.logic.GameInfo
class SimulationStep (gameInfo: GameInfo) {
var turns = gameInfo.turns
var victoryType = gameInfo.currentPlayerCiv.victoryManager.hasWonVictoryType()
var victoryType = gameInfo.currentPlayerCiv.victoryManager.getVictoryTypeAchieved()
var winner: String? = null
val currentPlayer = gameInfo.currentPlayer
// val durationString: String = formatDuration(Duration.ofMillis(System.currentTimeMillis() - startTime))

View File

@ -377,7 +377,9 @@ object TranslationFileWriter {
when {
// Promotion names are not uniques but since we did the "[unitName] ability"
// they need the "parameters" treatment too
(field.name == "uniques" || field.name == "promotions") && (fieldValue is java.util.AbstractCollection<*>) ->
// Same for victory milestones
(field.name == "uniques" || field.name == "promotions" || field.name == "milestones")
&& (fieldValue is java.util.AbstractCollection<*>) ->
for (item in fieldValue)
if (item is String) submitString(item, Unique(item)) else serializeElement(item!!)
fieldValue is java.util.AbstractCollection<*> ->
@ -456,6 +458,7 @@ object TranslationFileWriter {
"UnitPromotions" -> emptyArray<Promotion>().javaClass
"Units" -> emptyArray<BaseUnit>().javaClass
"UnitTypes" -> emptyArray<UnitType>().javaClass
"VictoryTypes" -> emptyArray<Victory>().javaClass
else -> this.javaClass // dummy value
}
}

View File

@ -322,6 +322,10 @@ fun String.tr(): String {
return fullyTranslatedString
}
if (contains('{')) { // Translating partial sentences
return curlyBraceRegex.replace(this) { it.groups[1]!!.value.tr() }
}
// There might still be optimization potential here!
if (contains('[')) { // Placeholders!
/**
@ -371,9 +375,6 @@ fun String.tr(): String {
return languageSpecificPlaceholder // every component is already translated
}
if (contains('{')) { // sentence
return curlyBraceRegex.replace(this) { it.groups[1]!!.value.tr() }
}
if (Stats.isStats(this)) return Stats.parse(this).toString()

View File

@ -8,6 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.*
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.models.ruleset.*
import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.INamed
import com.unciv.models.translations.tr
@ -177,13 +178,13 @@ class CivilopediaScreen(
val religionEnabled = if (game.isGameInfoInitialized()) game.gameInfo.isReligionEnabled()
else ruleset.beliefs.isNotEmpty()
val victoryTypes = if (game.isGameInfoInitialized()) game.gameInfo.gameParameters.victoryTypes
else VictoryType.values().toList()
else listOf()
fun shouldBeDisplayed(obj: IHasUniques): Boolean {
return when {
obj.hasUnique(UniqueType.HiddenFromCivilopedia) -> false
(!religionEnabled && obj.hasUnique(UniqueType.HiddenWithoutReligion)) -> false
obj.getMatchingUniques(UniqueType.HiddenWithoutVictoryType).any { !victoryTypes.contains(VictoryType.valueOf(it.params[0])) } -> false
obj.getMatchingUniques(UniqueType.HiddenWithoutVictoryType).any { !victoryTypes.contains(it.params[0]) } -> false
else -> true
}
}

View File

@ -5,7 +5,6 @@ import com.unciv.UncivGame
import com.unciv.logic.civilization.CityStateType
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
@ -135,7 +134,8 @@ class GameOptionsTable(
}
private fun Table.addMaxTurnsSlider(): UncivSlider? {
if (!gameParameters.victoryTypes.contains(VictoryType.Time)) return null
if (gameParameters.victoryTypes.none { ruleset.victories[it]?.enablesMaxTurns() == true })
return null
add("{Max Turns}:".toLabel()).left().expandX()
val slider = UncivSlider(250f, 1500f, 50f) {
@ -229,25 +229,23 @@ class GameOptionsTable(
add("{Victory Conditions}:".toLabel()).colspan(2).row()
// Create a checkbox for each VictoryType existing
var i = 0
val victoryConditionsTable = Table().apply { defaults().pad(5f) }
for (victoryType in VictoryType.values()) {
if (victoryType == VictoryType.Neutral) continue
val victoryCheckbox = victoryType.name.toCheckBox(gameParameters.victoryTypes.contains(victoryType)) {
for ((i, victoryType) in ruleset.victories.values.withIndex()) {
val victoryCheckbox = victoryType.name.toCheckBox(gameParameters.victoryTypes.contains(victoryType.name)) {
// If the checkbox is checked, adds the victoryTypes else remove it
if (it) {
gameParameters.victoryTypes.add(victoryType)
gameParameters.victoryTypes.add(victoryType.name)
} else {
gameParameters.victoryTypes.remove(victoryType)
gameParameters.victoryTypes.remove(victoryType.name)
}
// show or hide the max turns select box
if (victoryType == VictoryType.Time)
if (victoryType.enablesMaxTurns())
update()
}
victoryCheckbox.name = victoryType.name
victoryCheckbox.isDisabled = locked
victoryConditionsTable.add(victoryCheckbox).left()
if (++i % 2 == 0) victoryConditionsTable.row()
if ((i + 1) % 2 == 0) victoryConditionsTable.row()
}
add(victoryConditionsTable).colspan(2).row()
}

View File

@ -41,6 +41,10 @@ class NewGameScreen(
init {
updateRuleset() // must come before playerPickerTable so mod nations from fromSettings
// Has to be initialized before the mapOptionsTable, since the mapOptionsTable refers to it on init
if (gameSetupInfo.gameParameters.victoryTypes.isEmpty())
gameSetupInfo.gameParameters.victoryTypes.addAll(ruleset.victories.keys)
playerPickerTable = PlayerPickerTable(
this, gameSetupInfo.gameParameters,
if (isNarrowerThan4to3()) stage.width - 20f else 0f
@ -92,6 +96,14 @@ class NewGameScreen(
noHumanPlayersPopup.open()
return@onClick
}
if (gameSetupInfo.gameParameters.victoryTypes.isEmpty()) {
val noVictoryTypesPopup = Popup(this)
noVictoryTypesPopup.addGoodSizedLabel("No victory conditions were selected!".tr()).row()
noVictoryTypesPopup.addCloseButton()
noVictoryTypesPopup.open()
return@onClick
}
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!

View File

@ -10,7 +10,6 @@ import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.QuestName
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.CivilopediaCategories
@ -108,7 +107,7 @@ class WonderOverviewTab(
wonder.name in startingObsolete -> false
wonder.getMatchingUniques(UniqueType.HiddenWithoutVictoryType)
.any { unique ->
!gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(unique.params[0]))
!gameInfo.gameParameters.victoryTypes.contains(unique.params[0])
} -> false
else -> wonderEra <= viewerEra
}

View File

@ -2,16 +2,12 @@ package com.unciv.ui.victoryscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.tr
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.CompletionStatus
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.newgamescreen.NewGameScreen
import com.unciv.ui.pickerscreens.PickerScreen
@ -22,10 +18,7 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
val gameInfo = worldScreen.gameInfo
private val playerCivInfo = worldScreen.viewingCiv
val victoryTypes = gameInfo.gameParameters.victoryTypes
private val scientificVictoryEnabled = victoryTypes.contains(VictoryType.Scientific)
private val culturalVictoryEnabled = victoryTypes.contains(VictoryType.Cultural)
private val dominationVictoryEnabled = victoryTypes.contains(VictoryType.Domination)
val enabledVictoryTypes = gameInfo.gameParameters.victoryTypes
private val contentsTable = Table()
@ -35,7 +28,7 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
stage.addActor(difficultyLabel)
val tabsTable = Table().apply { defaults().pad(10f) }
val setMyVictoryButton = "Our status".toTextButton().onClick { setMyVictoryTable() }
val setMyVictoryButton = "Our status".toTextButton().onClick { setOurVictoryTable() }
if (!playerCivInfo.isSpectator()) tabsTable.add(setMyVictoryButton)
val setGlobalVictoryButton = "Global status".toTextButton().onClick { setGlobalVictoryTable() }
tabsTable.add(setGlobalVictoryButton)
@ -48,28 +41,27 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
if (playerCivInfo.isSpectator())
setGlobalVictoryTable()
else
setMyVictoryTable()
setOurVictoryTable()
rightSideButton.isVisible = false
var someoneHasWon = false
val playerVictoryType = playerCivInfo.victoryManager.hasWonVictoryType()
val playerVictoryType = playerCivInfo.victoryManager.getVictoryTypeAchieved()
if (playerVictoryType != null) {
someoneHasWon = true
wonOrLost("You have won a [${playerVictoryType.name}] Victory!")
wonOrLost("You have won a [$playerVictoryType] Victory!", playerVictoryType, true)
}
for (civ in gameInfo.civilizations.filter { it.isMajorCiv() && it != playerCivInfo }) {
val civVictoryType = civ.victoryManager.hasWonVictoryType()
val civVictoryType = civ.victoryManager.getVictoryTypeAchieved()
if (civVictoryType != null) {
someoneHasWon = true
val winningCivName = civ.civName
wonOrLost("[$winningCivName] has won a [${civVictoryType.name}] Victory!")
wonOrLost("[${civ.civName}] has won a [$civVictoryType] Victory!", civVictoryType, false)
}
}
if (playerCivInfo.isDefeated()) {
wonOrLost("")
wonOrLost("", null, false)
} else if (!someoneHasWon) {
setDefaultCloseAction()
onBackButtonClicked { game.setWorldScreen() }
@ -77,18 +69,15 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
}
private fun wonOrLost(description: String) {
// description will be empty when the player loses - no parameters - so this will be when(null) and end up in the else branch:
val endGameMessage = when (description.getPlaceholderParameters().firstOrNull()) {
VictoryType.Time.name -> "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!"
VictoryType.Cultural.name -> "You have achieved victory through the awesome power of your Culture. Your civilization's greatness - the magnificence of its monuments and the power of its artists - have astounded the world! Poets will honor you as long as beauty brings gladness to a weary heart."
VictoryType.Domination.name -> "The world has been convulsed by war. Many great and powerful civilizations have fallen, but you have survived - and emerged victorious! The world will long remember your glorious triumph!"
VictoryType.Scientific.name -> "You have achieved victory through mastery of Science! You have conquered the mysteries of nature and led your people on a voyage to a brave new world! Your triumph will be remembered as long as the stars burn in the night sky!"
VictoryType.Diplomatic.name -> "You have triumphed over your foes through the art of diplomacy! Your cunning and wisdom have earned you great friends - and divided and sown confusion among your enemies! Forever will you be remembered as the leader who brought peace to this weary world!"
VictoryType.Neutral.name -> "Your civilization stands above all others! The exploits of your people shall be remembered until the end of civilization itself!"
else -> "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
}
private fun wonOrLost(description: String, victoryType: String?, hasWon: Boolean) {
val endGameMessage =
when {
hasWon && (victoryType == null || victoryType !in gameInfo.ruleSet.victories) -> "Your civilization stands above all others! The exploits of your people shall be remembered until the end of civilization itself!"
victoryType == null || victoryType !in gameInfo.ruleSet.victories -> "You have been defeated. Your civilization has been overwhelmed by its many foes. But your people do not despair, for they know that one day you shall return - and lead them forward to victory!"
hasWon -> playerCivInfo.gameInfo.ruleSet.victories[victoryType]!!.victoryString
else -> playerCivInfo.gameInfo.ruleSet.victories[victoryType]!!.defeatString
}
descriptionLabel.setText(description.tr() + "\n" + endGameMessage.tr())
rightSideButton.setText("Start new game".tr())
@ -107,134 +96,81 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
}
}
private fun setMyVictoryTable() {
val myVictoryStatusTable = Table()
myVictoryStatusTable.defaults().pad(10f)
if (scientificVictoryEnabled) myVictoryStatusTable.add("Science victory".toLabel())
if (culturalVictoryEnabled) myVictoryStatusTable.add("Cultural victory".toLabel())
if (dominationVictoryEnabled) myVictoryStatusTable.add("Conquest victory".toLabel())
myVictoryStatusTable.row()
if (scientificVictoryEnabled) myVictoryStatusTable.add(scienceVictoryColumn())
if (culturalVictoryEnabled) myVictoryStatusTable.add(culturalVictoryColumn())
if (dominationVictoryEnabled) myVictoryStatusTable.add(conquestVictoryColumn())
myVictoryStatusTable.row()
if (scientificVictoryEnabled) myVictoryStatusTable.add("Complete all the spaceship parts\n to win!".toLabel())
if (culturalVictoryEnabled) myVictoryStatusTable.add("Complete 5 policy branches and build\n the Utopia Project to win!".toLabel())
if (dominationVictoryEnabled) myVictoryStatusTable.add("Destroy all enemies\n to win!".toLabel())
contentsTable.clear()
contentsTable.add(myVictoryStatusTable)
}
private fun scienceVictoryColumn(): Table {
val t = Table()
t.defaults().pad(5f)
t.add(getMilestone("Built Apollo Program",
playerCivInfo.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts))).row()
val victoryManager = playerCivInfo.victoryManager
for (key in victoryManager.requiredSpaceshipParts.keys)
for (i in 0 until victoryManager.requiredSpaceshipParts[key]!!)
t.add(getMilestone(key, victoryManager.currentsSpaceshipParts[key]!! > i)).row() //(key, builtSpaceshipParts)
return t
}
private fun culturalVictoryColumn(): Table {
val t = Table()
t.defaults().pad(5f)
for (branch in playerCivInfo.gameInfo.ruleSet.policyBranches.values) {
val finisher = branch.policies.last().name
t.add(getMilestone(finisher, playerCivInfo.policies.isAdopted(finisher))).row()
private fun setOurVictoryTable() {
val ourVictoryStatusTable = Table()
ourVictoryStatusTable.defaults().pad(10f)
val victoriesToShow = gameInfo.getEnabledVictories()
for (victory in victoriesToShow) {
ourVictoryStatusTable.add("[${victory.key}] Victory".toLabel())
}
return t
ourVictoryStatusTable.row()
for (victory in victoriesToShow) {
ourVictoryStatusTable.add(getOurVictoryColumn(victory.key))
}
ourVictoryStatusTable.row()
for (victory in victoriesToShow) {
ourVictoryStatusTable.add(victory.value.victoryScreenHeader.toLabel())
}
contentsTable.clear()
contentsTable.add(ourVictoryStatusTable)
}
private fun conquestVictoryColumn(): Table {
private fun getOurVictoryColumn(victory: String): Table {
val victoryObject = gameInfo.ruleSet.victories[victory]!!
val table = Table()
table.defaults().pad(5f)
for (civ in playerCivInfo.gameInfo.civilizations) {
if (civ.isCurrentPlayer() || !civ.isMajorCiv()) continue
val civName =
if (playerCivInfo.diplomacy.containsKey(civ.civName)) civ.civName
else Constants.unknownNationName
table.add(getMilestone("Destroy [$civName]", civ.isDefeated())).row()
var firstIncomplete: Boolean = true
for (milestone in victoryObject.milestoneObjects) {
val completionStatus =
when {
milestone.hasBeenCompletedBy(playerCivInfo) -> CompletionStatus.Completed
firstIncomplete -> {
firstIncomplete = false
CompletionStatus.Partially
}
else -> CompletionStatus.Incomplete
}
for (button in milestone.getVictoryScreenButtons(completionStatus, playerCivInfo)) {
table.add(button).row()
}
}
return table
}
fun getMilestone(text: String, achieved: Boolean): TextButton {
val textButton = text.toTextButton()
if (achieved) textButton.color = Color.GREEN
else textButton.color = Color.GRAY
return textButton
}
private fun setGlobalVictoryTable() {
val majorCivs = gameInfo.civilizations.filter { it.isMajorCiv() }
val globalVictoryTable = Table().apply { defaults().pad(10f) }
if (scientificVictoryEnabled) globalVictoryTable.add(getGlobalScientificVictoryColumn(majorCivs))
if (culturalVictoryEnabled) globalVictoryTable.add(getGlobalCulturalVictoryColumn(majorCivs))
if (dominationVictoryEnabled) globalVictoryTable.add(getGlobalDominationVictoryColumn(majorCivs))
val victoriesToShow = gameInfo.ruleSet.victories.filter { !it.value.hiddenInVictoryScreen && enabledVictoryTypes.contains(it.key) }
for (victory in victoriesToShow) {
globalVictoryTable.add(getGlobalVictoryColumn(majorCivs, victory.key))
}
contentsTable.clear()
contentsTable.add(globalVictoryTable)
}
private fun getGlobalVictoryColumn(majorCivs: List<CivilizationInfo>, victory: String): Table {
val victoryColumn = Table().apply { defaults().pad(10f) }
victoryColumn.add("[$victory] Victory".toLabel()).row()
victoryColumn.addSeparator()
private fun getGlobalDominationVictoryColumn(majorCivs: List<CivilizationInfo>): Table {
val dominationVictoryColumn = Table().apply { defaults().pad(10f) }
dominationVictoryColumn.add("Undefeated civs".toLabel()).row()
dominationVictoryColumn.addSeparator()
for (civ in majorCivs.filter { !it.isDefeated() })
dominationVictoryColumn.add(getCivGroup(civ, "", playerCivInfo)).fillX().row()
for (civ in majorCivs.filter { it.isDefeated() })
dominationVictoryColumn.add(getCivGroup(civ, "", playerCivInfo)).fillX().row()
return dominationVictoryColumn
}
private fun getGlobalCulturalVictoryColumn(majorCivs: List<CivilizationInfo>): Table {
val policyVictoryColumn = Table().apply { defaults().pad(10f) }
policyVictoryColumn.add("Branches completed".toLabel()).row()
policyVictoryColumn.addSeparator()
data class CivToBranchesCompleted(val civ: CivilizationInfo, val branchesCompleted: Int)
val civsToBranchesCompleted = majorCivs.map {
CivToBranchesCompleted(it, it.policies.adoptedPolicies.count { pol -> Policy.isBranchCompleteByName(pol) })
}.sortedByDescending { it.branchesCompleted }
for (entry in civsToBranchesCompleted) {
val civToBranchesHaveCompleted = getCivGroup(entry.civ, " - " + entry.branchesCompleted, playerCivInfo)
policyVictoryColumn.add(civToBranchesHaveCompleted).fillX().row()
}
return policyVictoryColumn
}
private fun getGlobalScientificVictoryColumn(majorCivs: List<CivilizationInfo>): Table {
val scientificVictoryColumn = Table().apply { defaults().pad(10f) }
scientificVictoryColumn.add("Spaceship parts remaining".toLabel()).row()
scientificVictoryColumn.addSeparator()
data class civToSpaceshipPartsRemaining(val civ: CivilizationInfo, val partsRemaining: Int)
val civsToPartsRemaining = majorCivs.map {
civToSpaceshipPartsRemaining(it,
it.victoryManager.spaceshipPartsRemaining())
for (civ in majorCivs.filter { !it.isDefeated() }.sortedByDescending { it.victoryManager.amountMilestonesCompleted(victory) }) {
val buttonText = civ.victoryManager.getNextMilestone(victory)?.getVictoryScreenButtonHeaderText(false, civ) ?: "Done!"
victoryColumn.add(getCivGroup(civ, "\n" + buttonText.tr(), playerCivInfo)).fillX().row()
}
for (entry in civsToPartsRemaining) {
val civToPartsBeRemaining = (getCivGroup(entry.civ, " - " + entry.partsRemaining, playerCivInfo))
scientificVictoryColumn.add(civToPartsBeRemaining).fillX().row()
for (civ in majorCivs.filter { it.isDefeated() }.sortedByDescending { it.victoryManager.amountMilestonesCompleted(victory) }) {
val buttonText = civ.victoryManager.getNextMilestone(victory)?.getVictoryScreenButtonHeaderText(false, civ) ?: "Done!"
victoryColumn.add(getCivGroup(civ, "\n" + buttonText.tr(), playerCivInfo)).fillX().row()
}
return scientificVictoryColumn
return victoryColumn
}
private fun setCivRankingsTable() {
@ -257,36 +193,38 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
contentsTable.add(civRankingsTable)
}
companion object {
fun getCivGroup(civ: CivilizationInfo, afterCivNameText:String, currentPlayer:CivilizationInfo): Table {
val civGroup = Table()
private fun getCivGroup(civ: CivilizationInfo, afterCivNameText: String, currentPlayer: CivilizationInfo): Table {
val civGroup = Table()
var labelText = civ.civName.tr()+afterCivNameText
var labelColor = Color.WHITE
val backgroundColor: Color
var labelText = "{${civ.civName.tr()}}{${afterCivNameText.tr()}}"
var labelColor = Color.WHITE
val backgroundColor: Color
if (civ.isDefeated()) {
civGroup.add(ImageGetter.getImage("OtherIcons/DisbandUnit")).size(30f)
backgroundColor = Color.LIGHT_GRAY
labelColor = Color.BLACK
} else if (currentPlayer == civ // || game.viewEntireMapForDebug
|| currentPlayer.knows(civ) || currentPlayer.isDefeated() || currentPlayer.victoryManager.hasWon()) {
civGroup.add(ImageGetter.getNationIndicator(civ.nation, 30f))
backgroundColor = civ.nation.getOuterColor()
labelColor = civ.nation.getInnerColor()
} else {
civGroup.add(ImageGetter.getRandomNationIndicator(30f))
backgroundColor = Color.DARK_GRAY
labelText = Constants.unknownNationName
}
civGroup.background = ImageGetter.getRoundedEdgeRectangle(backgroundColor)
val label = labelText.toLabel(labelColor)
label.setAlignment(Align.center)
civGroup.add(label).padLeft(10f)
civGroup.pack()
return civGroup
if (civ.isDefeated()) {
civGroup.add(ImageGetter.getImage("OtherIcons/DisbandUnit")).size(30f)
backgroundColor = Color.LIGHT_GRAY
labelColor = Color.BLACK
} else if (currentPlayer == civ // || game.viewEntireMapForDebug
|| currentPlayer.knows(civ)
|| currentPlayer.isDefeated()
|| currentPlayer.victoryManager.hasWon()
) {
civGroup.add(ImageGetter.getNationIndicator(civ.nation, 30f))
backgroundColor = civ.nation.getOuterColor()
labelColor = civ.nation.getInnerColor()
} else {
civGroup.add(ImageGetter.getRandomNationIndicator(30f))
backgroundColor = Color.DARK_GRAY
labelText = Constants.unknownNationName
}
civGroup.background = ImageGetter.getRoundedEdgeRectangle(backgroundColor)
val label = labelText.toLabel(labelColor)
label.setAlignment(Align.center)
civGroup.add(label).padLeft(10f)
civGroup.pack()
return civGroup
}
}