diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/BattleConfigScreen.kt b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/BattleConfigScreen.kt index 90ea64696..c563809d3 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/BattleConfigScreen.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/BattleConfigScreen.kt @@ -217,20 +217,33 @@ private fun BattleConfigContent( .height(IntrinsicSize.Min) ) { PartySelection( - config = config + config = config, + modifier = Modifier.weight(1f) ) VerticalDivider() ServerSelection( - config = config + config = config, + modifier = Modifier.weight(1f) ) + } + HorizontalDivider() + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(IntrinsicSize.Min) + ) { + ExitOffScriptScreen( + config = config, + modifier = Modifier.weight(1f) + ) VerticalDivider() OutOfCommandsExitScreen( config = config, - modifier = Modifier.weight(2f) + modifier = Modifier.weight(1f) ) } diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ExitOffScript.kt b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ExitOffScript.kt new file mode 100644 index 000000000..43607eeb9 --- /dev/null +++ b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ExitOffScript.kt @@ -0,0 +1,134 @@ +package io.github.fate_grand_automata.ui.battle_config_item + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.fate_grand_automata.R +import io.github.fate_grand_automata.prefs.core.BattleConfigCore +import io.github.fate_grand_automata.ui.dialog.FgaDialog +import io.github.fate_grand_automata.ui.prefs.remember + +@Composable +fun ExitOffScriptScreen( + modifier: Modifier = Modifier, + config: BattleConfigCore +) { + var exitOnOffScript by config.exitOnOffScript.remember() + + val dialog = FgaDialog() + + dialog.build( + color = MaterialTheme.colorScheme.background + ) { + title(stringResource(R.string.p_battle_config_exit_on_off_script)) + + message(text = stringResource(R.string.p_battle_config_exit_on_off_script_message)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding( + horizontal = 4.dp + ) + ) { + Card( + shape = RoundedCornerShape(25), + colors = CardDefaults.cardColors( + containerColor = + if (exitOnOffScript) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceVariant + ), + onClick = { + exitOnOffScript = true + dialog.hide() + }, + modifier = Modifier + .weight(1f) + .padding(horizontal = 2.dp) + ) { + Text( + text = stringResource(R.string.state_on).uppercase(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + textAlign = TextAlign.Center, + fontWeight = if (exitOnOffScript) FontWeight.Bold else null + ) + } + Card( + shape = RoundedCornerShape(25), + colors = CardDefaults.cardColors( + containerColor = + if (!exitOnOffScript) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceVariant + ), + onClick = { + exitOnOffScript = false + dialog.hide() + }, + modifier = Modifier + .weight(1f) + .padding(horizontal = 2.dp) + ) { + Text( + text = stringResource(R.string.state_off).uppercase(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + textAlign = TextAlign.Center, + fontWeight = if (!exitOnOffScript) FontWeight.Bold else null + ) + } + } + } + + Column( + modifier = modifier + .fillMaxSize() + .clickable( + onClick = { dialog.show() } + ) + .padding( + horizontal = 0.dp, + vertical = 4.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.p_battle_config_exit_on_off_script).uppercase(), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + maxLines = 1 + ) + Text( + text = when (exitOnOffScript) { + true -> stringResource(R.string.state_on).uppercase() + false -> stringResource(R.string.state_off).uppercase() + }, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/PartySelection.kt b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/PartySelection.kt index b544c3ade..848215bcd 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/PartySelection.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/PartySelection.kt @@ -28,7 +28,10 @@ import io.github.fate_grand_automata.ui.dialog.FgaDialog import io.github.fate_grand_automata.ui.prefs.remember @Composable -fun PartySelection(config: BattleConfigCore) { +fun PartySelection( + modifier: Modifier = Modifier, + config: BattleConfigCore, +) { var party by config.party.remember() val dialog = FgaDialog() @@ -55,7 +58,7 @@ fun PartySelection(config: BattleConfigCore) { } Column( - modifier = Modifier + modifier = modifier .fillMaxHeight() .clickable(onClick = { dialog.show() }) .padding(16.dp, 5.dp), diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ServerSelection.kt b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ServerSelection.kt index 4f761c356..d124068ad 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ServerSelection.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/battle_config_item/ServerSelection.kt @@ -26,7 +26,10 @@ import io.github.fate_grand_automata.ui.prefs.remember import io.github.fate_grand_automata.util.stringRes @Composable -fun ServerSelection(config: BattleConfigCore) { +fun ServerSelection( + modifier: Modifier = Modifier, + config: BattleConfigCore +) { var server by config.server.remember() val dialog = FgaDialog() @@ -65,7 +68,7 @@ fun ServerSelection(config: BattleConfigCore) { } Column( - modifier = Modifier + modifier = modifier .fillMaxHeight() .clickable(onClick = { dialog.show() }) .padding(16.dp, 5.dp), diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/exit/BattleExit.kt b/app/src/main/java/io/github/fate_grand_automata/ui/exit/BattleExit.kt index d8bc0ef98..d6859f26f 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/exit/BattleExit.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/exit/BattleExit.kt @@ -78,6 +78,7 @@ private fun AutoBattle.ExitReason.text(): String = when (this) { AutoBattle.ExitReason.Paused -> stringResource(R.string.script_paused) AutoBattle.ExitReason.StopAfterThisRun -> stringResource(R.string.stop_after_this_run) AutoBattle.ExitReason.ExitOnOutOfCommands -> stringResource(R.string.p_stop_on_out_of_commands) + AutoBattle.ExitReason.ExitOnOffScript -> stringResource(id = R.string.p_stop_on_off_script) } @Composable diff --git a/app/src/main/res/values/localized.xml b/app/src/main/res/values/localized.xml index d3db9a8f4..5d6d47041 100644 --- a/app/src/main/res/values/localized.xml +++ b/app/src/main/res/values/localized.xml @@ -418,6 +418,9 @@ After pressing on the button, switch the app filter from \"Not optimized\" to \" Out of Commands. Exiting Early for manual override. "Out of Commands Exit" "Exit Battle when ran out of commands" + "Off-script Exit" + "Exit Battle when a turn/wave that is not part of the script have been reached." + Off-script. Exiting Early for manual override. On Off diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt index 4c01ebb18..e3fcd52b7 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt @@ -33,6 +33,8 @@ internal class BattleConfig( override var exitOnOutOfCommands by prefs.exitOnOutOfCommands + override var exitOnOffScript by prefs.exitOnOffScript + override val server by prefs.server.map( defaultValue = null, convert = { it.asGameServer() }, diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt index d4f5cc1c0..f81f6cb02 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt @@ -187,4 +187,7 @@ class BattleConfigCore( val autoChooseTarget = maker.bool("auto_choose_target") val exitOnOutOfCommands = maker.bool("exit_on_out_of_commands") + + + val exitOnOffScript = maker.bool("exit_on_off_script") } \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/entrypoints/AutoBattle.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/entrypoints/AutoBattle.kt index ca2bcf5ea..ead600062 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/entrypoints/AutoBattle.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/entrypoints/AutoBattle.kt @@ -75,6 +75,7 @@ class AutoBattle @Inject constructor( data object Paused : ExitReason() data object StopAfterThisRun : ExitReason() data object ExitOnOutOfCommands : ExitReason() + data object ExitOnOffScript : ExitReason() } internal class BattleExitException(val reason: ExitReason) : Exception(reason.cause) diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/AutoSkillCommand.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/AutoSkillCommand.kt index f2c5a38c3..b39b7ee90 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/AutoSkillCommand.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/AutoSkillCommand.kt @@ -25,7 +25,11 @@ class AutoSkillCommand private constructor( fun commandTurnsUntilStage(stage: Int): Int = when (stage) { 0 -> stages[stage].flatten().size - else -> stages.subList(0, stage).flatten().size + else -> try { + stages.subList(0, stage).flatten().size + } catch (e: IndexOutOfBoundsException) { + stages.flatten().size + } } companion object { diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Battle.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Battle.kt index 8249ea4d9..ed1409e30 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Battle.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Battle.kt @@ -2,7 +2,6 @@ package io.github.fate_grand_automata.scripts.modules import io.github.fate_grand_automata.scripts.IFgoAutomataApi import io.github.fate_grand_automata.scripts.Images -import io.github.fate_grand_automata.scripts.ScriptLog import io.github.fate_grand_automata.scripts.entrypoints.AutoBattle import io.github.fate_grand_automata.scripts.models.NPUsage import io.github.fate_grand_automata.scripts.models.ParsedCard @@ -11,7 +10,6 @@ import io.github.fate_grand_automata.scripts.models.battle.BattleState import io.github.fate_grand_automata.scripts.prefs.IBattleConfig import io.github.lib_automata.dagger.ScriptScope import javax.inject.Inject -import kotlin.math.max import kotlin.time.Duration.Companion.seconds @ScriptScope @@ -26,7 +24,8 @@ class Battle @Inject constructor( private val skillSpam: SkillSpam, private val shuffleChecker: ShuffleChecker, private val stageTracker: StageTracker, - private val autoChooseTarget: AutoChooseTarget + private val autoChooseTarget: AutoChooseTarget, + private val commandTurnsTracker: CommandTurnsTracker ) : IFgoAutomataApi by api { init { prefs.stopAfterThisRun = false @@ -35,8 +34,6 @@ class Battle @Inject constructor( resetState() } - var outOfCommands = false - fun resetState() { // Don't increment no. of runs if we're just clicking on quest again and again // This can happen due to lags introduced during some events @@ -123,42 +120,7 @@ class Battle @Inject constructor( autoChooseTarget.choose() } - // It is already verified out of commands, no need to check further - if (outOfCommands) return@useSameSnapIn - - trackSkipTurns() - - outOfCommands = isOutOfCommand() - - if (outOfCommands && battleConfig.exitOnOutOfCommands) { - throw AutoBattle.BattleExitException(AutoBattle.ExitReason.ExitOnOutOfCommands) - } + commandTurnsTracker.trackTurns() } - private fun trackSkipTurns() { - if (!(state.stage > 0 && state.turn < 1)) return - - var commandTurnsUntilStage = autoSkill.commandTurnsUntilStage(state.stage) - - // add additional turn since it is checking at next stage/wave - commandTurnsUntilStage += 1 - - val skipTurns = max(0, commandTurnsUntilStage - (state.currentTurn + state.skipCommandTurns)) - - messages.log( - ScriptLog.TurnTrackingAtNewStage( - wave = state.stage, - currentTurn = state.currentTurn, - skipTurn = state.skipCommandTurns - ) - ) - - state.addSkipCommandTurns(skipTurns) - } - - private fun isOutOfCommand(): Boolean { - val totalCommandTurns = autoSkill.getTotalCommandTurns - - return (state.currentTurn + state.skipCommandTurns) > totalCommandTurns - } } diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/CommandTurnsTracker.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/CommandTurnsTracker.kt new file mode 100644 index 000000000..e7208f663 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/CommandTurnsTracker.kt @@ -0,0 +1,118 @@ +package io.github.fate_grand_automata.scripts.modules + +import io.github.fate_grand_automata.scripts.IFgoAutomataApi +import io.github.fate_grand_automata.scripts.ScriptLog +import io.github.fate_grand_automata.scripts.entrypoints.AutoBattle +import io.github.fate_grand_automata.scripts.models.battle.BattleState +import io.github.fate_grand_automata.scripts.prefs.IBattleConfig +import io.github.lib_automata.dagger.ScriptScope +import javax.inject.Inject +import kotlin.math.max + +@ScriptScope +class CommandTurnsTracker @Inject constructor( + api: IFgoAutomataApi, + private val state: BattleState, + private val battleConfig: IBattleConfig, + private val autoSkill: AutoSkill, +) : IFgoAutomataApi by api { + + private var outOfCommands = false + + fun trackTurns() { + // It is already verified out of commands, no need to check further + if (!outOfCommands) { + trackSkipTurns() + } + + isOffScript() + + outOfCommands = isOutOfCommand() + if (outOfCommands && battleConfig.exitOnOutOfCommands) { + throw AutoBattle.BattleExitException(AutoBattle.ExitReason.ExitOnOutOfCommands) + } + } + + + /** + * Track the number of turns to skip + * + * This will not run on the 1st stage, 1st turn + * + * commandTurnsUntilStage is the count of command turns from the start until the current stage. + * Added additional turn since it is checking at next stage/wave + * + * Now perform the calculation to determine the number of turns to skip + * If the number of turns to skip is greater than 0, add it to the current skip turns + */ + private fun trackSkipTurns() { + if (!(state.stage > 0 && state.turn < 1)) return + + var commandTurnsUntilStage = autoSkill.commandTurnsUntilStage(state.stage) + + // add additional turn since it is checking at next stage/wave + commandTurnsUntilStage += 1 + + val skipTurns = max(0, commandTurnsUntilStage - (state.currentTurn + state.skipCommandTurns)) + + messages.log( + ScriptLog.TurnTrackingAtNewStage( + wave = state.stage, + currentTurn = state.currentTurn, + skipTurn = state.skipCommandTurns + ) + ) + + state.addSkipCommandTurns(skipTurns) + } + + /** + * Check if the script is off script + * + * This will not run on the 1st stage, 1st turn + * + * commandTurnsUntilStage is the count of command turns from the start until the current stage. + * For example: + * - 1st wave has 1 command turn, commandTurnsUntilStage = 1 + * - 2nd wave has 1 command turn, commandTurnsUntilStage = 2 + * - 3rd wave has 2 command turns, commandTurnsUntilStage = 4 + * + * If at the 2nd wave, which has 2 command turns, and you reach the next turn in the same wave (3 turns) + * that would mean you are off script. + */ + private fun isOffScript() { + if (!battleConfig.exitOnOffScript) return + + // First Turn doesn't need to check + if (state.stage == 0 && state.turn < 1) return + + val commandTurnsUntilStage = autoSkill.commandTurnsUntilStage(state.stage) + + val offScript = commandTurnsUntilStage < (state.currentTurn + state.skipCommandTurns) + + + if (offScript) { + throw AutoBattle.BattleExitException(AutoBattle.ExitReason.ExitOnOffScript) + } + + } + + /** + * Check if the script is out of commands + * + * Special case, when there are no commands, it is considered out of commands + * Note: It is supposed to be 0, but idk why it is 1. + * Probably because of the start flag that is getting drop + * @see [SkillCommandGroup.kt] + * + * if the number of turns and skip turns is greater than the total command turns, it is considered out of commands + */ + private fun isOutOfCommand(): Boolean { + val totalCommandTurns = autoSkill.getTotalCommandTurns + + // If there are no commands, it is considered out of commands + if (totalCommandTurns <= 1) return true + + return (state.currentTurn + state.skipCommandTurns) > totalCommandTurns + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt index 0d4e33073..d1c04cf59 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt @@ -29,6 +29,7 @@ interface IBattleConfig { val server: GameServer? var exitOnOutOfCommands: Boolean + var exitOnOffScript: Boolean fun export(): Map