diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 90132f6245..1ffb5a9617 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -304,21 +304,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion return turns + (totalTurns * startPercent / 100) } - fun getYear(turnOffset: Int = 0): Int { - val turn = getEquivalentTurn() + turnOffset - val yearsToTurn = speed.yearsPerTurn - var year = speed.startYear - var i = 0 - var yearsPerTurn: Float - - while (i < turn) { - yearsPerTurn = (yearsToTurn.firstOrNull { i < it.untilTurn }?.yearInterval ?: yearsToTurn.last().yearInterval) - year += yearsPerTurn - ++i - } - - return year.toInt() - } + fun getYear(turnOffset: Int = 0) = speed.turnToYear(getEquivalentTurn() + turnOffset).toInt() fun calculateChecksum(): String { val oldChecksum = checksum diff --git a/core/src/com/unciv/models/ruleset/Speed.kt b/core/src/com/unciv/models/ruleset/Speed.kt index 7e779b77ee..82806c8e41 100644 --- a/core/src/com/unciv/models/ruleset/Speed.kt +++ b/core/src/com/unciv/models/ruleset/Speed.kt @@ -29,11 +29,39 @@ class Speed : RulesetObject(), IsPartOfGameInfoSerialization { var startYear: Float = -4000f var turns: ArrayList> = ArrayList() - data class YearsPerTurn(val yearInterval: Float, val untilTurn: Int) - val yearsPerTurn: ArrayList by lazy { - ArrayList().apply { - turns.forEach { this.add(YearsPerTurn(it["yearsPerTurn"]!!, it["untilTurn"]!!.toInt())) } + // These could be private but for RulesetValidator checking it + data class YearsPerTurn(val yearInterval: Float, val untilTurn: Int) { + internal constructor(rawRow: HashMap) : this(rawRow["yearsPerTurn"]!!, rawRow["untilTurn"]!!.toInt()) + } + val yearsPerTurn: ArrayList by lazy { turns.mapTo(ArrayList()) { YearsPerTurn(it) } } + + /** End of defined turn range, used for starting Era's `startPercent` calculation */ + fun numTotalTurns(): Int = yearsPerTurn.last().untilTurn + + /** Calculate a Year from a turn number. + * + * Note that years can have fractional parts and the integer part of the year for two consecutive turns _can_ be equal, + * but Unciv currently has no way to display that. This is left as Float to enable such display in the future, + * maybe as months, or even the 18+1 'months' of the mayan Haab'. + * + * @param turn The logical turn number, any offset from starting in an advanced Era already added in + */ + fun turnToYear(turn: Int): Float { + var year = startYear + var intervalStartTurn = 0 + val lastIntervalEndTurn = numTotalTurns() + for ((turnLength, intervalEndTurn) in yearsPerTurn) { + if (intervalStartTurn >= turn) break // ensure year isn't projected backwards for negative `turn` + if (turn <= intervalEndTurn || intervalEndTurn == lastIntervalEndTurn) { + // We can interpolate linearly within this interval and are done + year += (turn - intervalStartTurn) * turnLength + break + } + // Accumulate total length in years of this interval and move on to the following intervals. + year += (intervalEndTurn - intervalStartTurn) * turnLength + intervalStartTurn = intervalEndTurn } + return year } val statCostModifiers: EnumMap by lazy { @@ -81,6 +109,4 @@ class Speed : RulesetObject(), IsPartOfGameInfoSerialization { yield(FormattedLine("Start year: [" + ("{[${abs(startYear).toInt()}] " + (if (startYear < 0) "BC" else "AD") + "}]").tr())) }.toList() override fun getSortGroup(ruleset: Ruleset): Int = (modifier * 1000).toInt() - - fun numTotalTurns(): Int = yearsPerTurn.last().untilTurn } diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt index e0fde8e60c..11246b7fc5 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt @@ -84,7 +84,7 @@ internal class WorldScreenTopBarResources(topbar: WorldScreenTopBar) : ScalingTa val yearText = YearTextUtil.toYearText( civInfo.gameInfo.getYear(), civInfo.isLongCountDisplay() ) - turnsLabel.setText(Fonts.turn + " " + civInfo.gameInfo.turns.tr() + " | " + yearText) + turnsLabel.setText(Fonts.turn + "\u2004" + civInfo.gameInfo.turns.tr() + "\u2004|\u2004" + yearText) // U+2004: Three-Per-Em Space resourcesWrapper.clearChildren() val civResources = civInfo.getCivResourcesByName() diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt index db455a4936..9f65e0b0c9 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt @@ -60,7 +60,7 @@ internal class WorldScreenTopBarStats(topbar: WorldScreenTopBar) : ScalingTableW isTransform = false defaults().pad(defaultTopPad, defaultHorizontalPad, defaultBottomPad, defaultHorizontalPad) - + fun addStat( icon: String, label: Label, @@ -124,7 +124,7 @@ internal class WorldScreenTopBarStats(topbar: WorldScreenTopBar) : ScalingTableW resetScale() val nextTurnStats = civInfo.stats.statsForNextTurn - + goldLabel.setText(civInfo.gold.tr()) goldPerTurnLabel.setText(rateLabel(nextTurnStats.gold)) @@ -155,9 +155,9 @@ internal class WorldScreenTopBarStats(topbar: WorldScreenTopBar) : ScalingTableW // kotlin Float division by Zero produces `Float.POSITIVE_INFINITY`, not an exception val turnsToNextPolicy = (civInfo.policies.getCultureNeededForNextPolicy() - civInfo.policies.storedCulture) / nextTurnStats.culture cultureString += when { - turnsToNextPolicy <= 0f -> " (!)" // Can choose policy right now - nextTurnStats.culture <= 0 -> " (${Fonts.infinity})" // when you start the game, you're not producing any culture - else -> " (" + Fonts.turn + " " + ceil(turnsToNextPolicy).toInt().tr() + ")" + turnsToNextPolicy <= 0f -> "\u2004(!)" // Can choose policy right now + nextTurnStats.culture <= 0 -> "\u2004(${Fonts.infinity})" // when you start the game, you're not producing any culture + else -> "\u2004(" + Fonts.turn + "\u2009" + ceil(turnsToNextPolicy).toInt().tr() + ")" // U+2004: Three-Per-Em Space, U+2009: Thin Space } return cultureString } diff --git a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md index cb32241183..157429bbc9 100644 --- a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md +++ b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md @@ -105,12 +105,16 @@ Each speed can have the following attributes: ### Time interval per turn -The "turns" attribute defines the number of years passed between turns. The attribute consists of a list of hashmaps, each hashmaps in turn having 2 required attributes: "yearsPerTurn" (Float) and "untilTurn" (Integer) +The "turns" attribute defines the number of years passing between turns. The attribute consists of a list of objects, each having 2 required attributes: "yearsPerTurn" (Float) and "untilTurn" (Integer) -| Attribute | Type | Default | Notes | -|--------------|---------|----------|------------------------------------------------------------------------------------------| -| yearsPerTurn | Integer | Required | Number of years passed between turns | -| untilTurn | Integer | Required | Which turn that this "speed" is active until (if it is the last object, this is ignored) | +| Attribute | Type | Default | Notes | +|--------------|---------|----------|------------------------------------------------------------| +| yearsPerTurn | Integer | Required | Number of years passing between turns | +| untilTurn | Integer | Required | End of this interval (if it is the last object, see below) | + +For each row, "yearsPerTurn" is applied up to the "untilTurn"-1 to "untilTurn" step. +The last "untilTurn" in the list is ignored for year calculation, that is, if a game passes that turn number, years continue to increment by the "yearsPerTurn" of the last entry. +However, this is used when starting a game in a later Era: Era.startPercent is relative to the last "untilTurn". The code below is an example of a valid "turns" definition and it specifies that the first 50 turns of a game last for 60 years each, then the next 30 turns (and any played after the 80th) last for 40 years each. diff --git a/tests/src/com/unciv/testing/BasicTests.kt b/tests/src/com/unciv/testing/BasicTests.kt index 6b36a7a524..74d2bea7f4 100644 --- a/tests/src/com/unciv/testing/BasicTests.kt +++ b/tests/src/com/unciv/testing/BasicTests.kt @@ -4,6 +4,7 @@ package com.unciv.testing import com.badlogic.gdx.Gdx import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.GameInfo import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.GameSettings import com.unciv.models.ruleset.Ruleset @@ -328,4 +329,50 @@ class BasicTests { } return stats } + + @Test + fun turnToYearTest() { + // Pretty random choice, but ensures 'turn > last definition' and 'float to int rounding' is tested + val testData = mapOf( + "Quick" to mapOf( + 0 to -4000, + 100 to 800, + 200 to 1860, + 300 to 2020, + 5000 to 6720 + ), + "Standard" to mapOf( + 99 to -400, + 479 to 2039, + 999 to 2299 + ), + "Epic" to mapOf( + 66 to -2350, + 666 to 2008, + 4242 to 3796 + ), + "Marathon" to mapOf( + 222 to -1280, + 1111 to 1978, + 1400 to 2041 + ), + ) + val gameInfo = GameInfo() + gameInfo.ruleset = ruleset + Assert.assertEquals(0, ruleset.eras[gameInfo.gameParameters.startingEra]!!.startPercent) + var fails = 0 + + for ((speedName, tests) in testData) { + val speed = ruleset.speeds[speedName]!! + gameInfo.speed = speed + for ((turn, expected) in tests) { + val actual = gameInfo.getYear(turn) + if (actual == expected) continue + println("speed: $speedName, turn: $turn, expected: $expected, actual: $actual") + fails++ + } + } + + Assert.assertEquals("Some turn to year conversions do not match", 0, fails) + } }