mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-21 20:18:28 +07:00
No ruins undo (#10376)
* Encapsulate Undo functionality
* Fix Ruins-Undo exploit
* Reorg RuinsManager candidate determination
* Deep RuinsManager clone
* Revert "Fix Ruins-Undo exploit"
This reverts commit 6df6a1a071
.
This commit is contained in:
parent
c8365b8919
commit
11108112b5
@ -38,6 +38,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen
|
||||
import com.unciv.ui.screens.savescreens.LoadGameScreen
|
||||
import com.unciv.ui.screens.worldscreen.PlayerReadyScreen
|
||||
import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.clearUndoCheckpoints
|
||||
import com.unciv.ui.screens.worldscreen.WorldMapHolder
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
import com.unciv.ui.screens.worldscreen.unit.UnitTable
|
||||
@ -111,6 +112,11 @@ object GUI {
|
||||
return UncivGame.Current.worldScreen!!.selectedCiv
|
||||
}
|
||||
|
||||
/** Disable Undo (as in: forget the way back, but allow future undo checkpoints) */
|
||||
fun clearUndoCheckpoints() {
|
||||
UncivGame.Current.worldScreen?.clearUndoCheckpoints()
|
||||
}
|
||||
|
||||
private var keyboardAvailableCache: Boolean? = null
|
||||
/** Tests availability of a physical keyboard */
|
||||
val keyboardAvailable: Boolean
|
||||
|
@ -141,10 +141,13 @@ class ReligionManager : IsPartOfGameInfoSerialization {
|
||||
UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo)
|
||||
}
|
||||
|
||||
fun greatProphetsEarned(): Int = civInfo.civConstructions.boughtItemsWithIncreasingPrice[getGreatProphetEquivalent()?.name ?: ""]
|
||||
// Counter.get never returns null, but it needs the correct key type, which is non-nullable
|
||||
|
||||
// https://www.reddit.com/r/civ/comments/2m82wu/can_anyone_detail_the_finer_points_of_great/
|
||||
// Game files (globaldefines.xml)
|
||||
fun faithForNextGreatProphet(): Int {
|
||||
val greatProphetsEarned = civInfo.civConstructions.boughtItemsWithIncreasingPrice[getGreatProphetEquivalent()!!.name]
|
||||
val greatProphetsEarned = greatProphetsEarned()
|
||||
|
||||
var faithCost =
|
||||
(200 + 100 * greatProphetsEarned * (greatProphetsEarned + 1) / 2f) *
|
||||
|
@ -1,5 +1,4 @@
|
||||
package com.unciv.logic.civilization.managers
|
||||
// Why is this the only file in its own package?
|
||||
|
||||
import com.unciv.logic.IsPartOfGameInfoSerialization
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
@ -10,49 +9,54 @@ import com.unciv.models.ruleset.unique.UniqueTriggerActivation
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import kotlin.random.Random
|
||||
|
||||
class RuinsManager : IsPartOfGameInfoSerialization {
|
||||
var lastChosenRewards: MutableList<String> = mutableListOf("", "")
|
||||
private fun rememberReward(reward: String) {
|
||||
lastChosenRewards[0] = lastChosenRewards[1]
|
||||
lastChosenRewards[1] = reward
|
||||
}
|
||||
class RuinsManager(
|
||||
private var lastChosenRewards: MutableList<String> = mutableListOf("", "")
|
||||
) : IsPartOfGameInfoSerialization {
|
||||
|
||||
@Transient
|
||||
lateinit var civInfo: Civilization
|
||||
@Transient
|
||||
lateinit var validRewards: List<RuinReward>
|
||||
|
||||
fun clone(): RuinsManager {
|
||||
val toReturn = RuinsManager()
|
||||
toReturn.lastChosenRewards = lastChosenRewards
|
||||
return toReturn
|
||||
}
|
||||
fun clone() = RuinsManager(ArrayList(lastChosenRewards)) // needs to deep-clone (the List, not the Strings) so undo works
|
||||
|
||||
fun setTransients(civInfo: Civilization) {
|
||||
this.civInfo = civInfo
|
||||
validRewards = civInfo.gameInfo.ruleset.ruinRewards.values.toList()
|
||||
}
|
||||
|
||||
private fun rememberReward(reward: String) {
|
||||
lastChosenRewards[0] = lastChosenRewards[1]
|
||||
lastChosenRewards[1] = reward
|
||||
}
|
||||
|
||||
private fun getShuffledPossibleRewards(triggeringUnit: MapUnit): Iterable<RuinReward> {
|
||||
val stateForOnlyAvailableWhen = StateForConditionals(civInfo, unit = triggeringUnit, tile = triggeringUnit.getTile())
|
||||
val candidates =
|
||||
validRewards.asSequence()
|
||||
// Filter out what shouldn't be considered right now, before the random choice
|
||||
.filterNot { possibleReward ->
|
||||
possibleReward.name in lastChosenRewards
|
||||
|| civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties
|
||||
|| possibleReward.hasUnique(UniqueType.HiddenWithoutReligion) && !civInfo.gameInfo.isReligionEnabled()
|
||||
|| possibleReward.hasUnique(UniqueType.HiddenAfterGreatProphet) && civInfo.religionManager.greatProphetsEarned() > 0
|
||||
|| possibleReward.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
|
||||
.any { !it.conditionalsApply(stateForOnlyAvailableWhen) }
|
||||
}
|
||||
// This might be a dirty way to do this, but it works (we do have randomWeighted in CollectionExtensions, but below we
|
||||
// need to choose another when the first choice's TriggerActivations report failure, and that's simpler this way)
|
||||
// For each possible reward, this feeds (reward.weight) copies of this reward to the overall Sequence to implement 'weight'.
|
||||
.flatMap { reward -> generateSequence { reward }.take(reward.weight) }
|
||||
// Convert to List since Sequence.shuffled would do one anyway, Mutable so shuffle doesn't need to pull a copy
|
||||
.toMutableList()
|
||||
// The resulting List now gets shuffled, using a tile-based random to thwart save-scumming.
|
||||
// Note both Sequence.shuffled and Iterable.shuffled (with a 'd') always pull an extra copy of a MutableList internally, even if you feed them one.
|
||||
candidates.shuffle(Random(triggeringUnit.getTile().position.hashCode()))
|
||||
return candidates
|
||||
}
|
||||
|
||||
fun selectNextRuinsReward(triggeringUnit: MapUnit) {
|
||||
val tileBasedRandom = Random(triggeringUnit.getTile().position.toString().hashCode())
|
||||
val availableRewards = validRewards.filter { it.name !in lastChosenRewards }
|
||||
|
||||
// This might be a dirty way to do this, but it works.
|
||||
// For each possible reward, this creates a list with reward.weight amount of copies of this reward
|
||||
// These lists are then combined into a single list, and the result is shuffled.
|
||||
val possibleRewards = availableRewards.flatMap { reward -> List(reward.weight) { reward } }.shuffled(tileBasedRandom)
|
||||
|
||||
for (possibleReward in possibleRewards) {
|
||||
if (civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties) continue
|
||||
if (possibleReward.hasUnique(UniqueType.HiddenWithoutReligion) && !civInfo.gameInfo.isReligionEnabled()) continue
|
||||
if (possibleReward.hasUnique(UniqueType.HiddenAfterGreatProphet)
|
||||
&& (civInfo.civConstructions.boughtItemsWithIncreasingPrice[civInfo.religionManager.getGreatProphetEquivalent()?.name] ?: 0) > 0
|
||||
) continue
|
||||
|
||||
if (possibleReward.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
|
||||
.any { !it.conditionalsApply(StateForConditionals(civInfo, unit=triggeringUnit, tile = triggeringUnit.getTile()) ) })
|
||||
continue
|
||||
|
||||
for (possibleReward in getShuffledPossibleRewards(triggeringUnit)) {
|
||||
var atLeastOneUniqueHadEffect = false
|
||||
for (unique in possibleReward.uniqueObjects) {
|
||||
atLeastOneUniqueHadEffect =
|
||||
|
@ -239,10 +239,7 @@ open class Tile : IsPartOfGameInfoSerialization {
|
||||
if (isExplored) {
|
||||
// Disable the undo button if a new tile has been explored
|
||||
if (!exploredBy.contains(player.civName)) {
|
||||
if (GUI.isWorldLoaded()) {
|
||||
val worldScreen = GUI.getWorldScreen()
|
||||
worldScreen.preActionGameInfo = worldScreen.gameInfo
|
||||
}
|
||||
GUI.clearUndoCheckpoints()
|
||||
exploredBy = exploredBy.withItem(player.civName)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,8 @@ import com.unciv.ui.screens.overviewscreen.EspionageOverviewScreen
|
||||
import com.unciv.ui.screens.pickerscreens.PolicyPickerScreen
|
||||
import com.unciv.ui.screens.pickerscreens.TechButton
|
||||
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.canUndo
|
||||
import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.restoreUndoCheckpoint
|
||||
|
||||
|
||||
/** A holder for Tech, Policies and Diplomacy buttons going in the top left of the WorldScreen just under WorldScreenTopBar */
|
||||
@ -118,7 +119,7 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
|
||||
|
||||
private fun updateUndoButton() {
|
||||
// Don't show the undo button if there is no action to undo
|
||||
if (worldScreen.gameInfo != worldScreen.preActionGameInfo && worldScreen.canChangeState) {
|
||||
if (worldScreen.canUndo()) {
|
||||
undoButtonHolder.touchable = Touchable.enabled
|
||||
undoButtonHolder.actor = undoButton
|
||||
} else {
|
||||
@ -164,11 +165,6 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
|
||||
|
||||
private fun handleUndo() {
|
||||
undoButton.disable()
|
||||
Concurrency.run {
|
||||
// Most of the time we won't load this, so we only set transients once we see it's relevant
|
||||
worldScreen.preActionGameInfo.setTransients()
|
||||
worldScreen.preActionGameInfo.isUpToDate = worldScreen.gameInfo.isUpToDate
|
||||
game.loadGame(worldScreen.preActionGameInfo)
|
||||
}
|
||||
worldScreen.restoreUndoCheckpoint()
|
||||
}
|
||||
}
|
||||
|
39
core/src/com/unciv/ui/screens/worldscreen/UndoHandler.kt
Normal file
39
core/src/com/unciv/ui/screens/worldscreen/UndoHandler.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.unciv.ui.screens.worldscreen
|
||||
|
||||
import com.unciv.utils.Concurrency
|
||||
|
||||
/** Encapsulates the Undo functionality.
|
||||
*
|
||||
* Implementation is based on actively saving GameInfo clones and restoring them when needed.
|
||||
* For now, there's only one single Undo level (but the class signature could easily support more).
|
||||
*/
|
||||
class UndoHandler(private val worldScreen: WorldScreen) {
|
||||
private var preActionGameInfo = worldScreen.gameInfo
|
||||
|
||||
fun canUndo() = preActionGameInfo != worldScreen.gameInfo && worldScreen.canChangeState
|
||||
|
||||
fun recordCheckpoint() {
|
||||
preActionGameInfo = worldScreen.gameInfo.clone()
|
||||
}
|
||||
|
||||
fun restoreCheckpoint() {
|
||||
Concurrency.run {
|
||||
// Most of the time we won't load this, so we only set transients once we see it's relevant
|
||||
preActionGameInfo.setTransients()
|
||||
preActionGameInfo.isUpToDate = worldScreen.gameInfo.isUpToDate // Multiplayer!
|
||||
worldScreen.game.loadGame(preActionGameInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCheckpoints() {
|
||||
preActionGameInfo = worldScreen.gameInfo
|
||||
}
|
||||
|
||||
/** Simple readability proxies so the caller can pretend the interface exists directly on WorldScreen (imports ugly but calls neat) */
|
||||
companion object {
|
||||
fun WorldScreen.canUndo() = undoHandler.canUndo()
|
||||
fun WorldScreen.recordUndoCheckpoint() = undoHandler.recordCheckpoint()
|
||||
fun WorldScreen.restoreUndoCheckpoint() = undoHandler.restoreCheckpoint()
|
||||
fun WorldScreen.clearUndoCheckpoints() = undoHandler.clearCheckpoints()
|
||||
}
|
||||
}
|
@ -32,8 +32,6 @@ import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.components.MapArrowType
|
||||
import com.unciv.ui.components.MiscArrowTypes
|
||||
import com.unciv.ui.components.widgets.UnitGroup
|
||||
import com.unciv.ui.components.widgets.ZoomableScrollPane
|
||||
import com.unciv.ui.components.extensions.center
|
||||
import com.unciv.ui.components.extensions.colorFromRGB
|
||||
import com.unciv.ui.components.extensions.darken
|
||||
@ -48,9 +46,12 @@ import com.unciv.ui.components.tilegroups.TileGroup
|
||||
import com.unciv.ui.components.tilegroups.TileGroupMap
|
||||
import com.unciv.ui.components.tilegroups.TileSetStrings
|
||||
import com.unciv.ui.components.tilegroups.WorldTileGroup
|
||||
import com.unciv.ui.components.widgets.UnitGroup
|
||||
import com.unciv.ui.components.widgets.ZoomableScrollPane
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.basescreen.UncivStage
|
||||
import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.recordUndoCheckpoint
|
||||
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.battleAnimation
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.utils.Log
|
||||
@ -278,7 +279,7 @@ class WorldMapHolder(
|
||||
}
|
||||
|
||||
|
||||
worldScreen.preActionGameInfo = worldScreen.gameInfo.clone()
|
||||
worldScreen.recordUndoCheckpoint()
|
||||
|
||||
launchOnGLThread {
|
||||
try {
|
||||
|
@ -131,8 +131,7 @@ class WorldScreen(
|
||||
|
||||
private var uiEnabled = true
|
||||
|
||||
var preActionGameInfo = gameInfo
|
||||
|
||||
internal val undoHandler = UndoHandler(this)
|
||||
|
||||
init {
|
||||
// notifications are right-aligned, they take up only as much space as necessary.
|
||||
|
@ -28,6 +28,7 @@ import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.widgets.UnitGroup
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.clearUndoCheckpoints
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.battleAnimation
|
||||
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.getHealthBar
|
||||
@ -285,7 +286,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
// There was a direct worldScreen.update() call here, removing its 'private' but not the comment justifying the modifier.
|
||||
// My tests (desktop only) show the red-flash animations look just fine without.
|
||||
worldScreen.shouldUpdate = true
|
||||
worldScreen.preActionGameInfo = worldScreen.gameInfo // Reset - can no longer undo
|
||||
worldScreen.clearUndoCheckpoints()
|
||||
//Gdx.graphics.requestRendering() // Use this if immediate rendering is required
|
||||
|
||||
if (!canStillAttack) return
|
||||
|
Loading…
Reference in New Issue
Block a user