mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-06-05 13:52:54 +00:00
Setup resume/quit options if match gets disconnected before it finishes.
This commit is contained in:
parent
a9c354ad8a
commit
2201b7d0fe
@ -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) {
|
||||
|
||||
@ -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
|
||||
@ -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<PVPDataModel>
|
||||
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<PVPDataModel>
|
||||
}
|
||||
@ -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<PVPDataModel> = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage)
|
||||
val call: Call<PVPDataModel> = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, action)
|
||||
|
||||
// Use the enqueue() method of the Call object to
|
||||
// make an asynchronous API request.
|
||||
|
||||
@ -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<MediaPlayer?>(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<com.github.nacabaro.vbhelper.battle.PVPDataModel?>(null) }
|
||||
var pendingOpponentForResume by remember { mutableStateOf<APIBattleCharacter?>(null) }
|
||||
var pendingCardIdForResume by remember { mutableStateOf<String?>(null) }
|
||||
var pendingApiStageForResume by remember { mutableStateOf<Int?>(null) }
|
||||
// Store the original opponent from the match (not the clicked one)
|
||||
var originalMatchOpponent by remember { mutableStateOf<APIBattleCharacter?>(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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user