From 2201b7d0feb5380a939b65e826491c56cd948c23 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 21 Jan 2026 20:23:51 -0500 Subject: [PATCH] Setup resume/quit options if match gets disconnected before it finishes. --- .../vbhelper/battle/ArenaBattleSystem.kt | 6 +- .../nacabaro/vbhelper/battle/PVPDataModel.kt | 3 +- .../nacabaro/vbhelper/battle/PVPService.kt | 11 +- .../vbhelper/battle/RetrofitHelper.kt | 6 +- .../vbhelper/screens/BattlesScreen.kt | 324 ++++++++++++++++-- 5 files changed, 310 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt index 728c17d..cac56be 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt @@ -160,9 +160,9 @@ class ArenaBattleSystem { _opponentHP = opponentHP } - fun initializeHP(playerMaxHP: Float, opponentMaxHP: Float) { - _playerHP = playerMaxHP - _opponentHP = opponentMaxHP + fun initializeHP(playerHP: Float, opponentHP: Float) { + _playerHP = playerHP + _opponentHP = opponentHP } fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt index e0b55f7..c30b47e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt @@ -9,5 +9,6 @@ data class PVPDataModel ( val playerAttackHit: Boolean, val playerAttackDamage: Int, val opponentAttackDamage: Int, - val winner: String + val winner: String, + val opponentCharaId: String? = null // TODO: Server will add this - opponent's charaId from the match ):java.io.Serializable \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt index fa7c19f..6b51431 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt @@ -9,5 +9,14 @@ interface PVPService { // This method returns a Call object with a generic // type of DataModel, which represents // the data model for the response. - fun getwinner(@Query("apiStage") apiStage: Int, @Query("playerID") playerID: Long, @Query("playerDigi") playerDigi: String, @Query("playerStage") playerStage: Int, @Query("critBar") critBar: Int, @Query("opponentDigi") opponentDigi: String, @Query("opponentStage") opponentStage: Int): Call + fun getwinner( + @Query("apiStage") apiStage: Int, + @Query("playerID") playerID: Long, + @Query("playerDigi") playerDigi: String, + @Query("playerStage") playerStage: Int, + @Query("critBar") critBar: Int, + @Query("opponentDigi") opponentDigi: String, + @Query("opponentStage") opponentStage: Int, + @Query("action") action: String? = null // Optional: "quit" or "rejoin" + ): Call } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt index f4771a4..b465ddc 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt @@ -256,6 +256,10 @@ class RetrofitHelper { */ fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) { + getPVPWinner(context, apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, null, callback) + } + + fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, action: String?, callback: (PVPDataModel) -> Unit) { try { // Create an authenticated Retrofit instance @@ -271,7 +275,7 @@ class RetrofitHelper { // Call the getwinner() method of the ApiService // to make an API request. - val call: Call = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage) + val call: Call = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, action) // Use the enqueue() method of the Call object to // make an asynchronous API request. diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt index f0db0cd..3d2cda4 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt @@ -95,6 +95,9 @@ import com.github.nacabaro.vbhelper.di.VBHelper import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.compose.material3.AlertDialog @Composable fun isLandscapeMode(): Boolean { @@ -238,10 +241,11 @@ fun BattleScreen( var mediaPlayer by remember { mutableStateOf(null) } // Initialize HP when battle starts + // Use currentHp if available (for resumed matches), otherwise use baseHp (for new matches) LaunchedEffect(activeCharacter, opponentCharacter) { - val playerMaxHP = activeCharacter?.baseHp?.toFloat() ?: 100f - val opponentMaxHP = opponentCharacter?.baseHp?.toFloat() ?: 100f - battleSystem.initializeHP(playerMaxHP, opponentMaxHP) + val playerHP = activeCharacter?.currentHp?.toFloat() ?: activeCharacter?.baseHp?.toFloat() ?: 100f + val opponentHP = opponentCharacter?.currentHp?.toFloat() ?: opponentCharacter?.baseHp?.toFloat() ?: 100f + battleSystem.initializeHP(playerHP, opponentHP) } // Start background music when battle starts @@ -805,17 +809,17 @@ fun MiddleBattleView( horizontalAlignment = getLandscapeHorizontalAlignment() ) { // Enemy HP bar (top) - LinearProgressIndicator( + LinearProgressIndicator( progress = { battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f) }, modifier = getLandscapeModifier(), color = Color.Red, - trackColor = Color.Gray - ) + trackColor = Color.Gray + ) Spacer(modifier = Modifier.height(4.dp)) // Enemy HP display numbers - Text( + Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", fontSize = getLandscapeFontSize(), color = Color.White, @@ -883,7 +887,7 @@ fun MiddleBattleView( (shake * shakeAmount.value).dp } else { 0.dp - } + } AnimatedSpriteImage( characterId = opponentCharacter?.charaId ?: "dim011_mon01", @@ -1292,7 +1296,7 @@ fun PlayerBattleView( (shake * shakeAmount.value).dp } else { 0.dp - } + } AnimatedSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", @@ -1652,6 +1656,15 @@ fun BattlesScreen() { // Random background set selection var selectedBackgroundSet by remember { mutableStateOf(0) } + // Resume/Quit match dialog state + var showResumeDialog by remember { mutableStateOf(false) } + var existingMatchState by remember { mutableStateOf(null) } + var pendingOpponentForResume by remember { mutableStateOf(null) } + var pendingCardIdForResume by remember { mutableStateOf(null) } + var pendingApiStageForResume by remember { mutableStateOf(null) } + // Store the original opponent from the match (not the clicked one) + var originalMatchOpponent by remember { mutableStateOf(null) } + // Sprite animation tester state /* var showSpriteTester by remember { mutableStateOf(false) } @@ -1726,15 +1739,15 @@ fun BattlesScreen() { try { // Create a new list to trigger UI recomposition opponentsList = ArrayList(opponents.opponentsList) - } catch (e: Exception) { - Log.d(TAG, "Error processing opponents data: ${e.message}") - e.printStackTrace() + } catch (e: Exception) { + Log.d(TAG, "Error processing opponents data: ${e.message}") + e.printStackTrace() + } } + } catch (e: Exception) { + Log.d(TAG,"Error calling getOpponents: ${e.message}") + e.printStackTrace() } - } catch (e: Exception) { - Log.d(TAG,"Error calling getOpponents: ${e.message}") - e.printStackTrace() - } } else { println("BATTLESCREEN: Cannot load opponents - activeUserCharacter: $currentCharacter") println("BATTLESCREEN: canBattle: $canBattle") @@ -1810,11 +1823,11 @@ fun BattlesScreen() { try { context.startActivity(authIntent) println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl") - } catch (e: Exception) { + } catch (e: Exception) { println("BATTLESCREEN: Failed to open auth URL: ${e.message}") - e.printStackTrace() - } - } + e.printStackTrace() + } + } } else { // For other errors, remove from processed set to allow retry with a new token println("BATTLESCREEN: Authentication failed, removing token from processed set to allow retry") @@ -2165,7 +2178,7 @@ fun BattlesScreen() { } val backButton = @Composable { - Button( + Button( onClick = { currentView = "main" } ) { Text("Back") @@ -2252,10 +2265,10 @@ fun BattlesScreen() { verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(opponentsList) { opponent -> - Button( - onClick = { + Button( + onClick = { activeCardId?.let { cardId -> - selectedOpponent = opponent + selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) @@ -2269,22 +2282,62 @@ fun BattlesScreen() { } RetrofitHelper().getPVPWinner(context, 0, userId ?: 2L, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult -> - // Update player character HP from API response - activeCharacter = activeCharacter?.copy( - baseHp = apiResult.playerHP, - currentHp = apiResult.playerHP - ) - currentView = "battle-main" + // Check if there's an existing match + when { + apiResult.status.contains("Existing match found", ignoreCase = true) -> { + // Show resume/quit dialog + // When resuming, we need to find the actual opponent from the match + // For now, we'll use the clicked opponent, but we need to look it up from opponentsList + // The server should return the opponent charaId in the response, but since it doesn't, + // we'll try to find it by matching the opponent HP or look it up after rejoin + existingMatchState = apiResult + pendingOpponentForResume = opponent + pendingCardIdForResume = cardId + pendingApiStageForResume = apiStage + // Store the clicked opponent temporarily, but we'll update it when resuming + originalMatchOpponent = null // Will be set when we rejoin + showResumeDialog = true + } + apiResult.status == "Match setup." -> { + // New match created - proceed normally + activeCharacter = activeCharacter?.copy( + baseHp = apiResult.playerHP, + currentHp = apiResult.playerHP + ) + currentView = "battle-main" + } + apiResult.status == "Match resumed." -> { + // Match was resumed (shouldn't happen on first call, but handle it) + activeCharacter = activeCharacter?.copy( + baseHp = apiResult.playerHP, + currentHp = apiResult.playerHP + ) + selectedOpponent = opponent.copy( + baseHp = apiResult.opponentHP, + currentHp = apiResult.opponentHP + ) + currentView = "battle-main" + } + else -> { + // Other status - log and proceed + println("BATTLESCREEN: Unexpected status: ${apiResult.status}") + activeCharacter = activeCharacter?.copy( + baseHp = apiResult.playerHP, + currentHp = apiResult.playerHP + ) + currentView = "battle-main" + } + } } } ?: run { println("BATTLESCREEN: No active card ID found in database") } }, modifier = Modifier.fillMaxWidth() - ) { - Text("Battle ${opponent.name}") - } - } + ) { + Text("Battle ${opponent.name}") + } + } } } else { Text("No opponents available for your stage", @@ -2422,9 +2475,212 @@ fun BattlesScreen() { } } } + + // Resume/Quit Match Dialog + if (showResumeDialog && existingMatchState != null) { + ResumeMatchDialog( + matchState = existingMatchState!!, + onResume = { + // User chose to resume - call API with action="rejoin" + // Note: We need to pass the opponent, but the server should use the one from the stored match + // After rejoin, we need to find the actual opponent from the opponents list + pendingCardIdForResume?.let { cardId -> + pendingOpponentForResume?.let { clickedOpponent -> + pendingApiStageForResume?.let { apiStage -> + RetrofitHelper().getPVPWinner( + context, + 0, + userId ?: 2L, + cardId, + apiStage, + 0, + clickedOpponent.charaId, + apiStage, + "rejoin" + ) { apiResult -> + println("BATTLESCREEN: Resuming match - opponentHP from API: ${apiResult.opponentHP}") + println("BATTLESCREEN: Clicked opponent: ${clickedOpponent.name} (${clickedOpponent.charaId}), baseHp: ${clickedOpponent.baseHp}") + + // Update player character HP from API response + activeCharacter = activeCharacter?.copy( + baseHp = apiResult.playerHP, + currentHp = apiResult.playerHP + ) + + // Find the actual opponent from the match + println("BATTLESCREEN: Checking for opponent charaId - opponentCharaId: ${apiResult.opponentCharaId}, winner: '${apiResult.winner}'") + val actualOpponent = if (apiResult.opponentCharaId != null) { + // Server provides opponent charaId - use it directly (most reliable) + println("BATTLESCREEN: Server provided opponent charaId: ${apiResult.opponentCharaId}") + opponentsList.find { it.charaId == apiResult.opponentCharaId } ?: run { + println("BATTLESCREEN: WARNING: Opponent charaId from server not found in opponentsList, using clicked opponent") + clickedOpponent + } + } else if (apiResult.winner.isNotEmpty() && apiResult.winner.contains("|")) { + println("BATTLESCREEN: Winner field contains pipe, attempting to parse: '${apiResult.winner}'") + // Try to extract opponent charaId from winner field + // Format appears to be: "playerDigi|playerStage|opponentDigi|opponentStage" + try { + val parts = apiResult.winner.split("|") + if (parts.size >= 3) { + val extractedOpponentCharaId = parts[2] // Third part is opponentDigi + println("BATTLESCREEN: Extracted opponent charaId from winner field: $extractedOpponentCharaId") + opponentsList.find { it.charaId == extractedOpponentCharaId } ?: run { + println("BATTLESCREEN: WARNING: Extracted opponent charaId not found in opponentsList, using clicked opponent") + clickedOpponent + } + } else { + println("BATTLESCREEN: Winner field format unexpected, falling back to HP matching") + null // Will fall through to HP matching + } + } catch (e: Exception) { + println("BATTLESCREEN: Error parsing winner field: ${e.message}") + null // Will fall through to HP matching + } + } else { + println("BATTLESCREEN: Winner field is empty or doesn't contain pipe, winner='${apiResult.winner}'") + null // Will fall through to HP matching + } ?: run { + // Fallback: Try to match by HP - but prioritize the clicked opponent if it matches + println("BATTLESCREEN: All opponent identification methods failed, using HP matching fallback") + run { + // First, check if the clicked opponent matches the HP criteria + // This is the most likely match since the user clicked on it + val clickedOpponentMatches = run { + val hpDiff = clickedOpponent.baseHp - apiResult.opponentHP + hpDiff >= 0 && hpDiff <= (clickedOpponent.baseHp * 0.5) + } + + if (clickedOpponentMatches) { + println("BATTLESCREEN: Clicked opponent matches HP criteria, using it") + clickedOpponent + } else { + // If clicked opponent doesn't match, search for others + println("BATTLESCREEN: Clicked opponent doesn't match HP, searching for match") + opponentsList.filter { opp -> + // Only consider opponents of the same stage + opp.stage == clickedOpponent.stage + }.find { opp -> + // Match by checking if the opponent's baseHp is >= the current opponentHP + // and the difference is reasonable (opponent has taken some damage but not too much) + val hpDiff = opp.baseHp - apiResult.opponentHP + hpDiff >= 0 && hpDiff <= (opp.baseHp * 0.5) // Allow up to 50% damage + } ?: run { + // If we can't find a match, try a broader search + println("BATTLESCREEN: Could not find opponent by HP matching, trying broader search") + opponentsList.find { opp -> + opp.stage == clickedOpponent.stage && + opp.baseHp >= apiResult.opponentHP + } ?: run { + println("BATTLESCREEN: Still no match, using clicked opponent as fallback") + clickedOpponent + } + } + } + } + } + + println("BATTLESCREEN: Selected opponent for resume: ${actualOpponent.name} (${actualOpponent.charaId}), baseHp: ${actualOpponent.baseHp}, currentHp: ${apiResult.opponentHP}") + + // Update opponent with correct HP from match + // Use the actual baseHp from the opponent, but set currentHp to the match HP + selectedOpponent = actualOpponent.copy( + baseHp = actualOpponent.baseHp, // Keep original baseHp + currentHp = apiResult.opponentHP // Use current HP from match + ) + + showResumeDialog = false + existingMatchState = null + originalMatchOpponent = selectedOpponent + currentView = "battle-main" + } + } + } + } + }, + onQuit = { + // User chose to quit - call API with action="quit" + pendingCardIdForResume?.let { cardId -> + pendingOpponentForResume?.let { opponent -> + pendingApiStageForResume?.let { apiStage -> + RetrofitHelper().getPVPWinner( + context, + 0, + userId ?: 2L, + cardId, + apiStage, + 0, + opponent.charaId, + apiStage, + "quit" + ) { apiResult -> + // New match created - proceed normally + activeCharacter = activeCharacter?.copy( + baseHp = apiResult.playerHP, + currentHp = apiResult.playerHP + ) + showResumeDialog = false + existingMatchState = null + currentView = "battle-main" + } + } + } + } + }, + onDismiss = { + showResumeDialog = false + existingMatchState = null + pendingOpponentForResume = null + pendingCardIdForResume = null + pendingApiStageForResume = null + } + ) + } } } +@Composable +fun ResumeMatchDialog( + matchState: com.github.nacabaro.vbhelper.battle.PVPDataModel, + onResume: () -> Unit, + onQuit: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Ongoing Match Found", fontWeight = FontWeight.Bold) + }, + text = { + Column { + Text("You have an ongoing match:") + Spacer(modifier = Modifier.height(8.dp)) + Text("Round: ${matchState.currentRound + 1}", fontWeight = FontWeight.Bold) + Text("Your HP: ${matchState.playerHP}") + Text("Opponent HP: ${matchState.opponentHP}") + Spacer(modifier = Modifier.height(8.dp)) + Text("What would you like to do?") + } + }, + confirmButton = { + Button( + onClick = onResume, + colors = ButtonDefaults.buttonColors(containerColor = Color.Green) + ) { + Text("Resume Match") + } + }, + dismissButton = { + Button( + onClick = onQuit, + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + ) { + Text("Quit & Start New") + } + } + ) +} + @Composable fun AnimatedBattleBackground( modifier: Modifier = Modifier