From a011ae39a40442f8e3bfbc98e14931b09e2f4500 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 1 Aug 2025 15:42:37 -0400 Subject: [PATCH 01/89] First push on branch. --- app/build.gradle.kts | 7 + app/src/main/AndroidManifest.xml | 2 + .../vbhelper/battle/APIBattleCharacter.kt | 13 + .../vbhelper/battle/OpponentService.kt | 13 + .../vbhelper/battle/OpponentsDataModel.kt | 5 + .../nacabaro/vbhelper/battle/PVPDataModel.kt | 13 + .../nacabaro/vbhelper/battle/PVPService.kt | 13 + .../vbhelper/battle/RetrofitHelper.kt | 179 ++++++++ .../vbhelper/screens/BattlesScreen.kt | 410 +++++++++++++++++- 9 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentService.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentsDataModel.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 753ad0e..00f6931 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,4 +91,11 @@ dependencies { implementation("com.google.android.material:material:1.2.0") implementation(libs.protobuf.javalite) implementation("androidx.compose.material:material") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") + + // HTTP request logging + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d3a5163..c28d47b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentsDataModel.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentsDataModel.kt new file mode 100644 index 0000000..8bff492 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentsDataModel.kt @@ -0,0 +1,5 @@ +package com.github.nacabaro.vbhelper.battle + +data class OpponentsDataModel ( + val opponentsList: ArrayList +):java.io.Serializable \ No newline at end of file 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 new file mode 100644 index 0000000..e0b55f7 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPDataModel.kt @@ -0,0 +1,13 @@ +package com.github.nacabaro.vbhelper.battle + +data class PVPDataModel ( + val status: String, + val state: Int, + val currentRound: Int, + val playerHP: Int, + val opponentHP: Int, + val playerAttackHit: Boolean, + val playerAttackDamage: Int, + val opponentAttackDamage: Int, + val winner: String +):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 new file mode 100644 index 0000000..8bc6ba2 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/PVPService.kt @@ -0,0 +1,13 @@ +package com.github.nacabaro.vbhelper.battle + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +interface PVPService { + @GET("api/pvp") + // 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: Int, @Query("playerDigi") playerDigi: String, @Query("playerStage") playerStage: Int, @Query("critBar") critBar: Int, @Query("opponentDigi") opponentDigi: String, @Query("opponentStage") opponentStage: Int): 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 new file mode 100644 index 0000000..1cfca0e --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt @@ -0,0 +1,179 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import retrofit2.Retrofit +import android.widget.Toast +import retrofit2.* +import retrofit2.converter.gson.GsonConverterFactory + +class RetrofitHelper { + + fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) { + println("RetrofitHelper: Starting API call for stage: $stage") + + try { + // Create a Retrofit instance with the base URL and + // a GsonConverterFactory for parsing the response. + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + GsonConverterFactory.create()).build() + println("RetrofitHelper: Retrofit instance created") + + // Create an ApiService instance from the Retrofit instance. + val service: OpponentService = retrofit.create(OpponentService::class.java) + println("RetrofitHelper: Service created") + + // Call the getopponents() method of the ApiService + // to make an API request. + val call: Call = service.getopponents(stage) + println("RetrofitHelper: API call created, enqueueing...") + + // Use the enqueue() method of the Call object to + // make an asynchronous API request. + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + println("RetrofitHelper: API call failed: ${t.message}") + t.printStackTrace() + Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + println("RetrofitHelper: API response received - Code: ${response.code()}") + println("RetrofitHelper: Response body: ${response.body()}") + + if(response.isSuccessful){ + println("RetrofitHelper: Response successful, calling callback") + val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel + callback(opponentsList) + } else { + println("RetrofitHelper: Response not successful - Error: ${response.errorBody()?.string()}") + } + } + }) + } catch (e: Exception) { + println("RetrofitHelper: Exception in getOpponents: ${e.message}") + e.printStackTrace() + } + } + + /* + fun getCombatWinner(context: Context, stage: String, callback: (CombatDataModel) -> Unit) { + + // Create a Retrofit instance with the base URL and + // a GsonConverterFactory for parsing the response. + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + GsonConverterFactory.create()).build() + + // Create an ApiService instance from the Retrofit instance. + val service: CombatService = retrofit.create(CombatService::class.java) + + // Call the getwinner() method of the ApiService + // to make an API request. + val call: Call = service.getwinner(stage) + + // Use the enqueue() method of the Call object to + // make an asynchronous API request. + call.enqueue(object : Callback { + // This is an anonymous inner class that implements the Callback interface. + + override fun onFailure(call: Call, t: Throwable) { + // This method is called when the API request fails. + Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + // This method is called when the API response is received successfully. + + if(response.isSuccessful){ + // If the response is successful, parse the + // response body to a DataModel object. + val winner: CombatDataModel = response.body() as CombatDataModel + + // Call the callback function with the DataModel + // object as a parameter. + callback(winner) + } + } + }) + } + + fun getBattleWinner(context: Context, playerDigi: String, playerStage: Int, opponentDigi: String, opponentStage: Int, callback: (BattleDataModel) -> Unit) { + + // Create a Retrofit instance with the base URL and + // a GsonConverterFactory for parsing the response. + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + GsonConverterFactory.create()).build() + + // Create an ApiService instance from the Retrofit instance. + val service: BattleService = retrofit.create(BattleService::class.java) + + // Call the getwinner() method of the ApiService + // to make an API request. + val call: Call = service.getwinner(playerDigi, playerStage, opponentDigi, opponentStage) + + // Use the enqueue() method of the Call object to + // make an asynchronous API request. + call.enqueue(object : Callback { + // This is an anonymous inner class that implements the Callback interface. + + override fun onFailure(call: Call, t: Throwable) { + // This method is called when the API request fails. + Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + // This method is called when the API response is received successfully. + + if(response.isSuccessful){ + // If the response is successful, parse the + // response body to a DataModel object. + val winner: BattleDataModel = response.body() as BattleDataModel + + // Call the callback function with the DataModel + // object as a parameter. + callback(winner) + } + } + }) + } + */ + + fun getPVPWinner(context: Context, apiStage: Int, playerID: Int, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) { + + // Create a Retrofit instance with the base URL and + // a GsonConverterFactory for parsing the response. + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + GsonConverterFactory.create()).build() + + // Create an ApiService instance from the Retrofit instance. + val service: PVPService = retrofit.create(PVPService::class.java) + + // Call the getwinner() method of the ApiService + // to make an API request. + val call: Call = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage) + + // Use the enqueue() method of the Call object to + // make an asynchronous API request. + call.enqueue(object : Callback { + // This is an anonymous inner class that implements the Callback interface. + + override fun onFailure(call: Call, t: Throwable) { + // This method is called when the API request fails. + Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + // This method is called when the API response is received successfully. + + if(response.isSuccessful){ + // If the response is successful, parse the + // response body to a DataModel object. + val apiResults: PVPDataModel = response.body() as PVPDataModel + + // Call the callback function with the DataModel + // object as a parameter. + callback(apiResults) + } + } + }) + } +} \ No newline at end of file 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 b50ef12..7a334be 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 @@ -9,10 +9,269 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.material3.Button +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.dp +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.platform.LocalContext +import androidx.compose.material3.ExperimentalMaterial3Api +import com.github.nacabaro.vbhelper.battle.APIBattleCharacter import com.github.nacabaro.vbhelper.components.TopBanner +import com.github.nacabaro.vbhelper.battle.RetrofitHelper +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BattlesScreen() { + var currentView by remember { mutableStateOf("main") } + + var opponentsList by remember { mutableStateOf(ArrayList()) } + + var activeCharacter by remember { mutableStateOf(null) } + + var expanded by remember { mutableStateOf(false) } + var selectedStage by remember { mutableStateOf("") } + + val context = LocalContext.current + + val rookieButton = @Composable { + Button( + onClick = { + println("Rookie button clicked - starting API call") + try { + RetrofitHelper().getOpponents(context, "rookie") { opponents -> + println("API call completed successfully") + try { + println("Received opponents data: $opponents") + println("Opponents list size: ${opponents.opponentsList.size}") + + // For loop to check opponents and print their names + for (opponent in opponents.opponentsList) { + println("Opponent: ${opponent.name}") + } + + // Store the opponents in your ArrayList + opponentsList.clear() + opponentsList.addAll(opponents.opponentsList) + + println("Updated opponentsList size: ${opponentsList.size}") + println("About to change view to rookie") + currentView = "rookie" + println("View changed to rookie") + } catch (e: Exception) { + println("Error processing opponents data: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + println("Error calling getOpponents: ${e.message}") + e.printStackTrace() + } + } + ) { + Text("Rookie Battles") + } + } + + val championButton = @Composable { + Button( + onClick = { + println("Champion button clicked - starting API call") + try { + RetrofitHelper().getOpponents(context, "champion") { opponents -> + println("API call completed successfully") + try { + println("Received opponents data: $opponents") + println("Opponents list size: ${opponents.opponentsList.size}") + + // For loop to check opponents and print their names + for (opponent in opponents.opponentsList) { + println("Opponent: ${opponent.name}") + } + + // Store the opponents in your ArrayList + opponentsList.clear() + opponentsList.addAll(opponents.opponentsList) + + println("Updated opponentsList size: ${opponentsList.size}") + println("About to change view to champion") + currentView = "champion" + println("View changed to champion") + } catch (e: Exception) { + println("Error processing opponents data: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + println("Error calling getOpponents: ${e.message}") + e.printStackTrace() + } + } + ) { + Text("Champion Battles") + } + } + + val ultimateButton = @Composable { + Button( + onClick = { + println("Ultimate button clicked - starting API call") + try { + RetrofitHelper().getOpponents(context, "ultimate") { opponents -> + println("API call completed successfully") + try { + println("Received opponents data: $opponents") + println("Opponents list size: ${opponents.opponentsList.size}") + + // For loop to check opponents and print their names + for (opponent in opponents.opponentsList) { + println("Opponent: ${opponent.name}") + } + + // Store the opponents in your ArrayList + opponentsList.clear() + opponentsList.addAll(opponents.opponentsList) + + println("Updated opponentsList size: ${opponentsList.size}") + println("About to change view to ultimate") + currentView = "ultimate" + println("View changed to ultimate") + } catch (e: Exception) { + println("Error processing opponents data: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + println("Error calling getOpponents: ${e.message}") + e.printStackTrace() + } + } + ) { + Text("Ultimate Battles") + } + } + + val megaButton = @Composable { + Button( + onClick = { + println("Mega button clicked - starting API call") + try { + RetrofitHelper().getOpponents(context, "mega") { opponents -> + println("API call completed successfully") + try { + println("Received opponents data: $opponents") + println("Opponents list size: ${opponents.opponentsList.size}") + + // For loop to check opponents and print their names + for (opponent in opponents.opponentsList) { + println("Opponent: ${opponent.name}") + } + + // Store the opponents in your ArrayList + opponentsList.clear() + opponentsList.addAll(opponents.opponentsList) + + println("Updated opponentsList size: ${opponentsList.size}") + println("About to change view to mega") + currentView = "mega" + println("View changed to mega") + } catch (e: Exception) { + println("Error processing opponents data: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + println("Error calling getOpponents: ${e.message}") + e.printStackTrace() + } + } + ) { + Text("Mega Battles") + } + } + + val backButton = @Composable { + Button( + onClick = { + currentView = "main" + } + ) { + Text("Back") + } + } + + val characterDropdown = @Composable { currentStage: String -> + // Create hardcoded character lists for each stage + val rookieCharacters = listOf( + APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("PULSEMON", "degimon_name_Dim000_003", "dim000_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("DORUMON", "degimon_name_dim137_mon03", "dim137_mon03", 0, 1, 3000, 3000, 5100.0f, 1050.0f) + ) + + val championCharacters = listOf( + APIBattleCharacter("GREYMON","degimon_name_Dim012_004","dim012_mon04",1,1,2000, 2000, 3000.0f,900.0f), + APIBattleCharacter("TYRANNOMON","degimon_name_Dim008_006","dim008_mon06",1,3,2000, 2000, 2400.0f,600.0f), + APIBattleCharacter("DORUGAMON","degimon_name_dim137_mon05","dim137_mon05",1,3,3500, 3500, 5200.0f,1200.0f) + ) + + val ultimateCharacters = listOf( + APIBattleCharacter("METALGREYMON (VIRUS)","degimon_name_Dim014_005","dim014_mon05",2,2,2640, 2640, 2450.0f,800.0f), + APIBattleCharacter("MAMEMON", "degimon_name_Dim000_005", "dim000_mon05", 2, 1, 3000, 3000, 4000.0f, 1000.0f), + APIBattleCharacter("DORUGREYMON","degimon_name_dim137_mon09","dim137_mon09",2,3,5000, 5000, 6400.0f,1400.0f) + ) + + val megaCharacters = listOf( + APIBattleCharacter("WARGREYMON","degimon_name_Dim012_014","dim012_mon14",3,1,3080, 3080, 3825.0f,800.0f), + APIBattleCharacter("SLAYERDRAMON","degimon_name_dim129_mon15","dim129_mon15",3,1,4800, 4800, 6300.0f,1950.0f), + APIBattleCharacter("BREAKDRAMON","degimon_name_dim129_mon17","dim129_mon17",3,2,6000, 6000, 4000.0f,1980.0f) + ) + + // Get the appropriate character list based on current stage + val characterList = when (currentStage.lowercase()) { + "rookie" -> rookieCharacters + "champion" -> championCharacters + "ultimate" -> ultimateCharacters + "mega" -> megaCharacters + else -> rookieCharacters + } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = selectedStage.ifEmpty { "Select Character" }, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.menuAnchor() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + characterList.forEach { character -> + DropdownMenuItem( + text = { Text(character.name) }, + onClick = { + selectedStage = character.name + activeCharacter = character + expanded = false + println("Selected character: ${character.name}") + } + ) + } + } + } + } + Scaffold ( topBar = { TopBanner( @@ -27,7 +286,156 @@ fun BattlesScreen() { .padding(top = contentPadding.calculateTopPadding()) .fillMaxSize() ) { - Text("Coming soon") + when (currentView) { + "main" -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + rookieButton() + championButton() + ultimateButton() + megaButton() + } + } + + + "rookie" -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Rookie Battle View") + + // Add character selection dropdown + characterDropdown("rookie") + + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter = opponent + println("Selected character: ${opponent.name}") + // You can add battle logic here + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } + } + + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } + + backButton() + } + } + + "champion" -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Champion Battle View") + + // Add character selection dropdown + characterDropdown("champion") + + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter = opponent + println("Selected character: ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } + } + + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } + + backButton() + } + } + + "ultimate" -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Ultimate Battle View") + + // Add character selection dropdown + characterDropdown("ultimate") + + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter = opponent + println("Selected character: ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } + } + + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } + + backButton() + } + } + + "mega" -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Mega Battle View") + + // Add character selection dropdown + characterDropdown("mega") + + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter = opponent + println("Selected character: ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } + } + + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } + + backButton() + } + } + } } } } From bd0cc4639818b9853a29d1f3b95c48c8a4f9767c Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 1 Aug 2025 15:53:40 -0400 Subject: [PATCH 02/89] Setup API calls for stage 0 on Digimon name button click. --- .../vbhelper/screens/BattlesScreen.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) 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 7a334be..5f7d810 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 @@ -312,9 +312,10 @@ fun BattlesScreen() { opponentsList.forEach { opponent -> Button( onClick = { - activeCharacter = opponent - println("Selected character: ${opponent.name}") - // You can add battle logic here + activeCharacter?.let { + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> + } + } }, modifier = Modifier.padding(vertical = 4.dp) ) { @@ -347,8 +348,10 @@ fun BattlesScreen() { opponentsList.forEach { opponent -> Button( onClick = { - activeCharacter = opponent - println("Selected character: ${opponent.name}") + activeCharacter?.let { + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> + } + } }, modifier = Modifier.padding(vertical = 4.dp) ) { @@ -381,8 +384,10 @@ fun BattlesScreen() { opponentsList.forEach { opponent -> Button( onClick = { - activeCharacter = opponent - println("Selected character: ${opponent.name}") + activeCharacter?.let { + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> + } + } }, modifier = Modifier.padding(vertical = 4.dp) ) { @@ -415,8 +420,10 @@ fun BattlesScreen() { opponentsList.forEach { opponent -> Button( onClick = { - activeCharacter = opponent - println("Selected character: ${opponent.name}") + activeCharacter?.let { + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> + } + } }, modifier = Modifier.padding(vertical = 4.dp) ) { From a72718273c01a29ae0c56ca22861b283022ebdca Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 1 Aug 2025 17:04:35 -0400 Subject: [PATCH 03/89] Added imports to remove build errors. Started added battle screens, logic, and animations. --- .../vbhelper/screens/BattlesScreen.kt | 562 ++++++++++++++++-- 1 file changed, 514 insertions(+), 48 deletions(-) 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 5f7d810..c270d2f 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 @@ -4,31 +4,481 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.material3.Button import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.draw.scale +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.Box import androidx.compose.ui.platform.LocalContext +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +//import androidx.compose.animation.core.animateFloatAsState +import kotlinx.coroutines.delay +import androidx.compose.foundation.background +import androidx.compose.ui.graphics.Color +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import com.github.nacabaro.vbhelper.battle.APIBattleCharacter +//import com.github.nacabaro.vbhelper.battle.BattleSpriteManager +import android.util.Log import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.battle.RetrofitHelper +class ArenaBattleSystem { + companion object { + private const val TAG = "VBArenaBattleSystem" + const val ANIMATION_DURATION = 1500L + } + + // Battle state + private var _playerCurrentHP by mutableStateOf(100f) + private var _playerMaxHP by mutableStateOf(100f) + private var _opponentCurrentHP by mutableStateOf(100f) + private var _opponentMaxHP by mutableStateOf(100f) + private var _isAttacking by mutableStateOf(false) + private var _attackProgress by mutableStateOf(0f) + private var _currentView by mutableStateOf(0) // 0 = player, 1 = opponent + private var _isAttackVisible by mutableStateOf(false) + private var _critBarProgress by mutableStateOf(0) + private var _isAttackButtonEnabled by mutableStateOf(true) + + // Exposed state for Compose + val playerCurrentHP: Float get() = _playerCurrentHP + val playerMaxHP: Float get() = _playerMaxHP + val opponentCurrentHP: Float get() = _opponentCurrentHP + val opponentMaxHP: Float get() = _opponentMaxHP + val isAttacking: Boolean get() = _isAttacking + val attackProgress: Float get() = _attackProgress + val currentView: Int get() = _currentView + val isAttackVisible: Boolean get() = _isAttackVisible + val critBarProgress: Int get() = _critBarProgress + val isAttackButtonEnabled: Boolean get() = _isAttackButtonEnabled + + //Initialize battle with character data + fun initializeBattle( + playerHP: Float = 100f, + opponentHP: Float = 100f, + playerMaxHP: Float = 100f, + opponentMaxHP: Float = 100f + ) { + _playerCurrentHP = playerHP + _playerMaxHP = playerMaxHP + _opponentCurrentHP = opponentHP + _opponentMaxHP = opponentMaxHP + _currentView = 0 + _isAttacking = false + _attackProgress = 0f + _isAttackVisible = false + _critBarProgress = 0 + _isAttackButtonEnabled = true + + Log.d(TAG, "Battle initialized: Player HP $playerHP/$playerMaxHP, Opponent HP $opponentHP/$opponentMaxHP") + } + + //Start player attack + fun startPlayerAttack() { + _isAttacking = true + _currentView = 0 + _isAttackVisible = true + _isAttackButtonEnabled = false + Log.d(TAG, "Player attack started") + } + + //Start opponent attack + fun startOpponentAttack() { + _isAttacking = true + _currentView = 1 + _isAttackVisible = true + Log.d(TAG, "Opponent attack started") + } + + //Update attack animation progress + fun updateAttackAnimation(progress: Float) { + _attackProgress = progress + } + + //Complete attack animation + fun completeAttackAnimation() { + _isAttacking = false + _isAttackVisible = false + _attackProgress = 0f + _currentView = if (_currentView == 0) 1 else 0 + _isAttackButtonEnabled = true + Log.d(TAG, "Attack animation completed") + } + + //Apply damage to player or opponent + fun applyDamage(isPlayer: Boolean, damage: Float) { + if (isPlayer) { + _playerCurrentHP = (_playerCurrentHP - damage).coerceAtLeast(0f) + Log.d(TAG, "Player took $damage damage. HP: ${_playerCurrentHP}/${_playerMaxHP}") + } else { + _opponentCurrentHP = (_opponentCurrentHP - damage).coerceAtLeast(0f) + Log.d(TAG, "Opponent took $damage damage. HP: ${_opponentCurrentHP}/${_opponentMaxHP}") + } + } + + //Update critical bar progress + fun updateCritBarProgress(progress: Int) { + _critBarProgress = progress + } + + + //Check if battle is over + fun isBattleOver(): Boolean { + return _playerCurrentHP <= 0f || _opponentCurrentHP <= 0f + } + + //Get battle winner + fun getWinner(): String? { + return when { + _playerCurrentHP <= 0f -> "opponent" + _opponentCurrentHP <= 0f -> "player" + else -> null + } + } + + //Reset battle state + fun resetBattle() { + _playerCurrentHP = _playerMaxHP + _opponentCurrentHP = _opponentMaxHP + _isAttacking = false + _attackProgress = 0f + _currentView = 0 + _isAttackVisible = false + _critBarProgress = 0 + _isAttackButtonEnabled = true + Log.d(TAG, "Battle reset") + } + + //Clean up resources + fun cleanup() { + _isAttacking = false + _isAttackVisible = false + _attackProgress = 0f + Log.d(TAG, "Battle system cleaned up") + } +} + +@Composable +fun BattleScreen( + battleSystem: ArenaBattleSystem, + stage: String = "rookie", + playerName: String = "Player", + opponentName: String = "Opponent", + onBattleComplete: (String?) -> Unit = {}, + onExitBattle: () -> Unit = {} +) { + var animationProgress by remember { mutableStateOf(0f) } + + // Critical bar timer + LaunchedEffect(Unit) { + while (true) { + delay(30) + if (!battleSystem.isAttacking) { + battleSystem.updateCritBarProgress((battleSystem.critBarProgress + 5) % 101) + } + } + } + + // Attack animation + LaunchedEffect(battleSystem.isAttacking) { + if (battleSystem.isAttacking) { + animationProgress = 0f + animate( + initialValue = 0f, + targetValue = 1f, + animationSpec = tween(ArenaBattleSystem.ANIMATION_DURATION.toInt()) + ) { value, _ -> + animationProgress = value + battleSystem.updateAttackAnimation(value) + } + battleSystem.completeAttackAnimation() + + // Check if battle is over + if (battleSystem.isBattleOver()) { + onBattleComplete(battleSystem.getWinner()) + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + when (battleSystem.currentView) { + 0 -> PlayerBattleView( + battleSystem = battleSystem, + stage = stage, + playerName = playerName, + attackAnimationProgress = animationProgress, + onAttackClick = { + battleSystem.startPlayerAttack() + // Apply damage after animation + battleSystem.applyDamage(false, 20f) // Opponent takes damage + } + ) + 1 -> OpponentBattleView( + battleSystem = battleSystem, + stage = stage, + opponentName = opponentName, + attackAnimationProgress = animationProgress + ) + } + + // Exit button + Button( + onClick = onExitBattle, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + ) { + Text("Exit", color = Color.White) + } + } +} + +@Composable +fun PlayerBattleView( + battleSystem: ArenaBattleSystem, + stage: String, + playerName: String, + attackAnimationProgress: Float, + onAttackClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + // Health display + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text("Current HP", color = Color.White, fontSize = 12.sp) + Text( + text = battleSystem.playerCurrentHP.toInt().toString(), + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + Column { + Text("Max HP", color = Color.White, fontSize = 12.sp) + Text( + text = battleSystem.playerMaxHP.toInt().toString(), + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + } + + // Health bar + LinearProgressIndicator( + progress = (battleSystem.playerCurrentHP / battleSystem.playerMaxHP).coerceIn(0f, 1f), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Player character with attack animation + Box( + modifier = Modifier.size(120.dp), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + when (stage) { + "rookie" -> R.drawable.agumon + "champion" -> R.drawable.greymon + "ultimate" -> R.drawable.doruguremon + "mega" -> R.drawable.machinedramon + else -> R.drawable.agumon + } + ), + contentDescription = "Player Character", + modifier = Modifier + .size(120.dp) + .scale(2f), + contentScale = ContentScale.Fit + ) + + // Attack animation overlay + if (attackAnimationProgress > 0) { + Image( + painter = painterResource(R.drawable.atk_l_00), + contentDescription = "Attack Animation", + modifier = Modifier + .size(60.dp) + .offset( + x = (attackAnimationProgress * 200 - 100).dp, + y = 0.dp + ), + contentScale = ContentScale.Fit + ) + } + } + + // Critical bar + LinearProgressIndicator( + progress = battleSystem.critBarProgress / 100f, + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Yellow, + trackColor = Color.Gray + ) + + // Attack button + Button( + onClick = onAttackClick, + enabled = battleSystem.isAttackButtonEnabled, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Red, + disabledContainerColor = Color.Gray + ), + shape = RoundedCornerShape(8.dp) + ) { + Text("Attack", color = Color.White, fontSize = 18.sp) + } + } +} + +@Composable +fun OpponentBattleView( + battleSystem: ArenaBattleSystem, + stage: String, + opponentName: String, + attackAnimationProgress: Float +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + // Health display + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text("Current HP", color = Color.White, fontSize = 12.sp) + Text( + text = battleSystem.opponentCurrentHP.toInt().toString(), + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + Column { + Text("Max HP", color = Color.White, fontSize = 12.sp) + Text( + text = battleSystem.opponentMaxHP.toInt().toString(), + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + } + + // Health bar + LinearProgressIndicator( + progress = (battleSystem.opponentCurrentHP / battleSystem.opponentMaxHP).coerceIn(0f, 1f), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Opponent character with attack animation + Box( + modifier = Modifier.size(120.dp), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + when (stage) { + "rookie" -> R.drawable.agumon + "champion" -> R.drawable.greymon + "ultimate" -> R.drawable.doruguremon + "mega" -> R.drawable.machinedramon + else -> R.drawable.agumon + } + ), + contentDescription = "Opponent Character", + modifier = Modifier + .size(120.dp) + .scale(-2f, 2f), // Flip horizontally + contentScale = ContentScale.Fit + ) + + // Attack animation overlay + if (attackAnimationProgress > 0) { + Image( + painter = painterResource(R.drawable.atk_l_00), + contentDescription = "Attack Animation", + modifier = Modifier + .size(60.dp) + .offset( + x = (-attackAnimationProgress * 200 + 100).dp, + y = 0.dp + ), + contentScale = ContentScale.Fit + ) + } + } + + // Spacer for layout balance + Spacer(modifier = Modifier.height(120.dp)) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun BattlesScreen() { + val TAG = "BattleScreen" + var currentView by remember { mutableStateOf("main") } var opponentsList by remember { mutableStateOf(ArrayList()) } @@ -43,34 +493,36 @@ fun BattlesScreen() { val rookieButton = @Composable { Button( onClick = { - println("Rookie button clicked - starting API call") + //println("Rookie button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "rookie") { opponents -> - println("API call completed successfully") + //println("API call completed successfully") try { - println("Received opponents data: $opponents") - println("Opponents list size: ${opponents.opponentsList.size}") + //println("Received opponents data: $opponents") + //println("Opponents list size: ${opponents.opponentsList.size}") // For loop to check opponents and print their names - for (opponent in opponents.opponentsList) { - println("Opponent: ${opponent.name}") - } + //for (opponent in opponents.opponentsList) { + // println("Opponent: ${opponent.name}") + //} // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - println("Updated opponentsList size: ${opponentsList.size}") - println("About to change view to rookie") + //println("Updated opponentsList size: ${opponentsList.size}") + //println("About to change view to rookie") currentView = "rookie" - println("View changed to rookie") + //println("View changed to rookie") } catch (e: Exception) { - println("Error processing opponents data: ${e.message}") + //println("Error processing opponents data: ${e.message}") + Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - println("Error calling getOpponents: ${e.message}") + //println("Error calling getOpponents: ${e.message}") + Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } } @@ -82,34 +534,36 @@ fun BattlesScreen() { val championButton = @Composable { Button( onClick = { - println("Champion button clicked - starting API call") + //println("Champion button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "champion") { opponents -> - println("API call completed successfully") + //println("API call completed successfully") try { - println("Received opponents data: $opponents") - println("Opponents list size: ${opponents.opponentsList.size}") + //println("Received opponents data: $opponents") + //println("Opponents list size: ${opponents.opponentsList.size}") // For loop to check opponents and print their names - for (opponent in opponents.opponentsList) { - println("Opponent: ${opponent.name}") - } + //for (opponent in opponents.opponentsList) { + // println("Opponent: ${opponent.name}") + //} // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - println("Updated opponentsList size: ${opponentsList.size}") - println("About to change view to champion") + //println("Updated opponentsList size: ${opponentsList.size}") + //println("About to change view to champion") currentView = "champion" - println("View changed to champion") + //println("View changed to champion") } catch (e: Exception) { - println("Error processing opponents data: ${e.message}") + //println("Error processing opponents data: ${e.message}") + Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - println("Error calling getOpponents: ${e.message}") + //println("Error calling getOpponents: ${e.message}") + Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } } @@ -121,34 +575,36 @@ fun BattlesScreen() { val ultimateButton = @Composable { Button( onClick = { - println("Ultimate button clicked - starting API call") + //println("Ultimate button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "ultimate") { opponents -> - println("API call completed successfully") + //println("API call completed successfully") try { - println("Received opponents data: $opponents") - println("Opponents list size: ${opponents.opponentsList.size}") + //println("Received opponents data: $opponents") + //println("Opponents list size: ${opponents.opponentsList.size}") // For loop to check opponents and print their names - for (opponent in opponents.opponentsList) { - println("Opponent: ${opponent.name}") - } + //for (opponent in opponents.opponentsList) { + // println("Opponent: ${opponent.name}") + //} // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - println("Updated opponentsList size: ${opponentsList.size}") - println("About to change view to ultimate") + //println("Updated opponentsList size: ${opponentsList.size}") + //println("About to change view to ultimate") currentView = "ultimate" - println("View changed to ultimate") + //println("View changed to ultimate") } catch (e: Exception) { - println("Error processing opponents data: ${e.message}") + //println("Error processing opponents data: ${e.message}") + Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - println("Error calling getOpponents: ${e.message}") + //println("Error calling getOpponents: ${e.message}") + Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } } @@ -160,34 +616,36 @@ fun BattlesScreen() { val megaButton = @Composable { Button( onClick = { - println("Mega button clicked - starting API call") + //println("Mega button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "mega") { opponents -> - println("API call completed successfully") + //println("API call completed successfully") try { - println("Received opponents data: $opponents") - println("Opponents list size: ${opponents.opponentsList.size}") + //println("Received opponents data: $opponents") + //println("Opponents list size: ${opponents.opponentsList.size}") // For loop to check opponents and print their names - for (opponent in opponents.opponentsList) { - println("Opponent: ${opponent.name}") - } + //for (opponent in opponents.opponentsList) { + // println("Opponent: ${opponent.name}") + //} // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - println("Updated opponentsList size: ${opponentsList.size}") - println("About to change view to mega") + //println("Updated opponentsList size: ${opponentsList.size}") + //println("About to change view to mega") currentView = "mega" - println("View changed to mega") + //println("View changed to mega") } catch (e: Exception) { - println("Error processing opponents data: ${e.message}") + //println("Error processing opponents data: ${e.message}") + Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - println("Error calling getOpponents: ${e.message}") + //println("Error calling getOpponents: ${e.message}") + Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } } @@ -442,6 +900,14 @@ fun BattlesScreen() { backButton() } } + + "battle-main" -> { + + } + + "battle-results" -> { + + } } } } From 9bcbe85b7fca3004e593416cb26370e0f586e5c4 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:25:48 -0400 Subject: [PATCH 04/89] Create SpriteImage.kt --- .../nacabaro/vbhelper/battle/SpriteImage.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt new file mode 100644 index 0000000..7956511 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt @@ -0,0 +1,41 @@ +package com.github.nacabaro.vbhelper.battle + +import androidx.compose.foundation.Image +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import com.github.nacabaro.vbhelper.battle.BattleSpriteManager + +@Composable +fun SpriteImage( + spriteName: String, + atlasName: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + val context = LocalContext.current + val spriteManager = remember { BattleSpriteManager(context) } + + var bitmap by remember { mutableStateOf(null) } + + LaunchedEffect(spriteName, atlasName) { + println("Loading sprite: $spriteName from atlas: $atlasName") + bitmap = spriteManager.loadSprite(spriteName, atlasName) + if (bitmap == null) { + println("Failed to load sprite: $spriteName from atlas: $atlasName") + } else { + println("Successfully loaded sprite: $spriteName from atlas: $atlasName") + } + } + + bitmap?.let { bmp -> + Image( + bitmap = bmp.asImageBitmap(), + contentDescription = "Sprite: $spriteName", + modifier = modifier, + contentScale = contentScale + ) + } +} \ No newline at end of file From c947e2519cb963d07458f33622c7aebb934bed10 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:25:54 -0400 Subject: [PATCH 05/89] Create AttackSpriteImage.kt --- .../vbhelper/components/AttackSpriteImage.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt new file mode 100644 index 0000000..9bf6b12 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt @@ -0,0 +1,44 @@ +package com.github.nacabaro.vbhelper.battle + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import com.github.nacabaro.vbhelper.battle.AttackSpriteManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun AttackSpriteImage( + characterId: String, + isLarge: Boolean = false, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + var bitmap by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + LaunchedEffect(characterId, isLarge) { + coroutineScope.launch { + val attackSpriteManager = AttackSpriteManager(context) + val loadedBitmap = withContext(Dispatchers.IO) { + attackSpriteManager.getAttackSprite(characterId, isLarge) + } + bitmap = loadedBitmap + } + } + + bitmap?.let { bmp -> + Image( + bitmap = bmp.asImageBitmap(), + contentDescription = "Attack Sprite", + modifier = modifier, + contentScale = contentScale + ) + } +} \ No newline at end of file From 8d9c5076454606c161f9e77ee4e9707245767dfe Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:25:57 -0400 Subject: [PATCH 06/89] Create SpriteFileManager.kt --- .../vbhelper/battle/SpriteFileManager.kt | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt new file mode 100644 index 0000000..e9769b8 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -0,0 +1,80 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +class SpriteFileManager(private val context: Context) { + + fun copySpriteFilesToInternalStorage() { + try { + // Create the base directory + val baseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + if (!baseDir.exists()) { + baseDir.mkdirs() + } + + // Copy files from assets to internal storage + copyAssetDirectory("Battle_Sprites_Reference/extracted_assets", baseDir) + + println("Sprite files copied successfully to: ${baseDir.absolutePath}") + + } catch (e: Exception) { + println("Error copying sprite files: ${e.message}") + e.printStackTrace() + } + } + + private fun copyAssetDirectory(assetPath: String, targetDir: File) { + try { + val assetManager = context.assets + val files = assetManager.list(assetPath) ?: return + + for (file in files) { + val assetFilePath = if (assetPath.isEmpty()) file else "$assetPath/$file" + val targetFile = File(targetDir, file) + + // Create subdirectories if needed + if (targetFile.parentFile != null && !targetFile.parentFile!!.exists()) { + targetFile.parentFile!!.mkdirs() + } + + // Check if it's a directory + val subFiles = assetManager.list(assetFilePath) + if (subFiles != null && subFiles.isNotEmpty()) { + // It's a directory, create it and copy contents + if (!targetFile.exists()) { + targetFile.mkdirs() + } + copyAssetDirectory(assetFilePath, targetFile) + } else { + // It's a file, copy it + copyAssetFile(assetFilePath, targetFile) + } + } + } catch (e: Exception) { + println("Error copying asset directory $assetPath: ${e.message}") + } + } + + private fun copyAssetFile(assetPath: String, targetFile: File) { + try { + val inputStream = context.assets.open(assetPath) + val outputStream = FileOutputStream(targetFile) + + inputStream.copyTo(outputStream) + inputStream.close() + outputStream.close() + + println("Copied: $assetPath -> ${targetFile.absolutePath}") + } catch (e: IOException) { + println("Error copying asset file $assetPath: ${e.message}") + } + } + + fun checkSpriteFilesExist(): Boolean { + val baseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + return baseDir.exists() && baseDir.listFiles()?.isNotEmpty() == true + } +} \ No newline at end of file From 09ee139addf4868427600675912487e2e5fb60ae Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:25:59 -0400 Subject: [PATCH 07/89] Create BattleSpriteManager.kt --- .../vbhelper/battle/BattleSpriteManager.kt | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt new file mode 100644 index 0000000..3de61f9 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -0,0 +1,164 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.google.gson.Gson +import java.io.File + +data class SpriteMapping( + val atlas_name: String, + val atlas_file: String, + val texture: TextureInfo, + val sprites: List +) + +data class TextureInfo( + val name: String, + val file: String, + val path_id: Long +) + +data class SpriteData( + val name: String, + val atlas_name: String, + val m_Name: String, + val texture_rect: TextureRect +) + +data class TextureRect( + val height: Float, + val width: Float, + val x: Float, + val y: Float +) + +class BattleSpriteManager(private val context: Context) { + private val gson = Gson() + private val spriteCache = mutableMapOf() + + // Base directory where your sprites are stored + private val spriteBaseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + + fun loadSprite(spriteName: String, atlasName: String): Bitmap? { + val cacheKey = "${spriteName}_${atlasName}" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + // Debug: Check if base directory exists + if (!spriteBaseDir.exists()) { + println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}") + return null + } + + println("Sprite base directory exists: ${spriteBaseDir.absolutePath}") + println("Available directories: ${spriteBaseDir.listFiles()?.map { it.name }}") + + try { + // Load the mapping file + val mappingFile = File(spriteBaseDir, "mappings/${atlasName}_mapping.json") + if (!mappingFile.exists()) { + println("Mapping file not found: ${mappingFile.absolutePath}") + return null + } + + val mappingJson = mappingFile.readText() + val mapping = gson.fromJson(mappingJson, SpriteMapping::class.java) + + // Load the PNG texture file + val textureFile = File(spriteBaseDir, "textures/${mapping.texture.file}") + if (!textureFile.exists()) { + println("Texture file not found: ${textureFile.absolutePath}") + return null + } + + val fullBitmap = BitmapFactory.decodeFile(textureFile.absolutePath) + if (fullBitmap == null) { + println("Failed to decode texture file: ${textureFile.absolutePath}") + return null + } + + // Load the specific sprite data file + val spriteDataFile = File(spriteBaseDir, "sprites/${atlasName}_sprite_${spriteName}.json") + if (!spriteDataFile.exists()) { + println("Sprite data file not found: ${spriteDataFile.absolutePath}") + return null + } + + val spriteDataJson = spriteDataFile.readText() + val spriteData = gson.fromJson(spriteDataJson, SpriteData::class.java) + + // Extract the sprite from the atlas using texture_rect coordinates + val spriteBitmap = Bitmap.createBitmap( + fullBitmap, + spriteData.texture_rect.x.toInt(), + spriteData.texture_rect.y.toInt(), + spriteData.texture_rect.width.toInt(), + spriteData.texture_rect.height.toInt() + ) + + // Cache the result + spriteCache[cacheKey] = spriteBitmap + + return spriteBitmap + + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + fun clearCache() { + spriteCache.clear() + } + + // Helper method to get available sprites for an atlas + fun getAvailableSprites(atlasName: String): List { + try { + val spritesDir = File(spriteBaseDir, "sprites") + if (!spritesDir.exists()) { + return emptyList() + } + + val spriteFiles = spritesDir.listFiles { file -> + file.name.startsWith("${atlasName}_sprite_") && file.name.endsWith(".json") + } ?: emptyArray() + + return spriteFiles.map { file -> + // Extract sprite number from filename (e.g., "dim000_mon01_sprite_00.json" -> "00") + val spriteNumber = file.name.substringAfter("_sprite_").substringBefore(".json") + spriteNumber + }.sorted() + + } catch (e: Exception) { + e.printStackTrace() + return emptyList() + } + } + + // Helper method to get available atlases + fun getAvailableAtlases(): List { + try { + val mappingsDir = File(spriteBaseDir, "mappings") + if (!mappingsDir.exists()) { + return emptyList() + } + + val mappingFiles = mappingsDir.listFiles { file -> + file.name.endsWith("_mapping.json") + } ?: emptyArray() + + return mappingFiles.map { file -> + // Extract atlas name from filename (e.g., "dim000_mon01_mapping.json" -> "dim000_mon01") + file.name.substringBefore("_mapping.json") + }.sorted() + + } catch (e: Exception) { + e.printStackTrace() + return emptyList() + } + } +} \ No newline at end of file From 1d2115519893be6efd1de580345f60a87fa38f2e Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:26:01 -0400 Subject: [PATCH 08/89] Create AttackSpriteManager.kt --- .../vbhelper/battle/AttackSpriteManager.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt new file mode 100644 index 0000000..301c35f --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -0,0 +1,87 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.google.gson.Gson +import java.io.File + +data class CharacterData( + val name: String, + val charaId: String, + val smalefilename: String, + val laugeFileName: String +) + +class AttackSpriteManager(private val context: Context) { + private val gson = Gson() + private val characterDataCache = mutableMapOf() + + // Base path for attack textures + private val attackTexturesPath = "Battle_Sprites_Reference/extracted_assets/atk_textures" + + fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { + try { + // Get character data + val characterData = getCharacterData(characterId) ?: return null + + // Determine which attack file to use + val attackFileName = if (isLarge) { + characterData.laugeFileName + } else { + characterData.smalefilename + } + + // Skip if no attack file + if (attackFileName == "0") return null + + // Load the attack sprite + val attackFilePath = "$attackTexturesPath/$attackFileName.png" + val attackFile = File(context.filesDir, attackFilePath) + + return if (attackFile.exists()) { + BitmapFactory.decodeFile(attackFile.absolutePath) + } else { + null + } + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + private fun getCharacterData(characterId: String): CharacterData? { + // Check cache first + if (characterDataCache.containsKey(characterId)) { + return characterDataCache[characterId] + } + + try { + // Load character data from JSON file + val characterDataFile = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats/character_data/CharacterData.json") + + if (!characterDataFile.exists()) { + return null + } + + val jsonContent = characterDataFile.readText() + // Parse the JSON and find the character with matching charaId + // This is a simplified version - you'll need to parse the actual JSON structure + + // For now, return a default character data + val characterData = CharacterData( + name = characterId, + charaId = characterId, + smalefilename = "atk_s_02", // Default small attack + laugeFileName = "atk_l_04" // Default large attack + ) + + characterDataCache[characterId] = characterData + return characterData + + } catch (e: Exception) { + e.printStackTrace() + return null + } + } +} \ No newline at end of file From a044d24f5f685d440345c42bb7c6520010cab7b2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 06:26:27 -0400 Subject: [PATCH 09/89] Updated BattleScreen.kt. Now starts battle and allow clicking of first attack button. --- .../vbhelper/screens/BattlesScreen.kt | 149 ++++++++++++++++-- 1 file changed, 140 insertions(+), 9 deletions(-) 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 c270d2f..d249003 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 @@ -49,6 +49,9 @@ import com.github.nacabaro.vbhelper.battle.APIBattleCharacter import android.util.Log import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.battle.RetrofitHelper +import com.github.nacabaro.vbhelper.battle.SpriteImage +import com.github.nacabaro.vbhelper.battle.AttackSpriteImage +import com.github.nacabaro.vbhelper.battle.SpriteFileManager class ArenaBattleSystem { companion object { @@ -192,6 +195,7 @@ fun BattleScreen( stage: String = "rookie", playerName: String = "Player", opponentName: String = "Opponent", + activeCharacter: APIBattleCharacter? = null, onBattleComplete: (String?) -> Unit = {}, onExitBattle: () -> Unit = {} ) { @@ -243,13 +247,15 @@ fun BattleScreen( battleSystem.startPlayerAttack() // Apply damage after animation battleSystem.applyDamage(false, 20f) // Opponent takes damage - } + }, + activeCharacter = activeCharacter ) 1 -> OpponentBattleView( battleSystem = battleSystem, stage = stage, opponentName = opponentName, - attackAnimationProgress = animationProgress + attackAnimationProgress = animationProgress, + activeCharacter = activeCharacter ) } @@ -272,7 +278,8 @@ fun PlayerBattleView( stage: String, playerName: String, attackAnimationProgress: Float, - onAttackClick: () -> Unit + onAttackClick: () -> Unit, + activeCharacter: APIBattleCharacter? = null ) { Column( modifier = Modifier @@ -321,6 +328,21 @@ fun PlayerBattleView( modifier = Modifier.size(120.dp), contentAlignment = Alignment.Center ) { + SpriteImage( + spriteName = "00", // The sprite number (00, 01, 02, etc.) + atlasName = when (stage) { + "rookie" -> "dim000_mon01" + "champion" -> "dim012_mon04" + "ultimate" -> "dim137_mon09" + "mega" -> "dim012_mon14" + else -> "dim000_mon01" + }, + modifier = Modifier + .size(120.dp) + .scale(2f), + contentScale = ContentScale.Fit + ) + /* Image( painter = painterResource( when (stage) { @@ -337,9 +359,22 @@ fun PlayerBattleView( .scale(2f), contentScale = ContentScale.Fit ) + */ // Attack animation overlay if (attackAnimationProgress > 0) { + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim000_mon03", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = (attackAnimationProgress * 200 - 100).dp, + y = 0.dp + ), + contentScale = ContentScale.Fit + ) + /* Image( painter = painterResource(R.drawable.atk_l_00), contentDescription = "Attack Animation", @@ -351,6 +386,7 @@ fun PlayerBattleView( ), contentScale = ContentScale.Fit ) + */ } } @@ -387,7 +423,8 @@ fun OpponentBattleView( battleSystem: ArenaBattleSystem, stage: String, opponentName: String, - attackAnimationProgress: Float + attackAnimationProgress: Float, + activeCharacter: APIBattleCharacter? = null ) { Column( modifier = Modifier @@ -436,6 +473,21 @@ fun OpponentBattleView( modifier = Modifier.size(120.dp), contentAlignment = Alignment.Center ) { + SpriteImage( + spriteName = "00", // The sprite number (00, 01, 02, etc.) + atlasName = when (stage) { + "rookie" -> "dim000_mon01" + "champion" -> "dim012_mon04" + "ultimate" -> "dim137_mon09" + "mega" -> "dim012_mon14" + else -> "dim000_mon01" + }, + modifier = Modifier + .size(120.dp) + .scale(2f), + contentScale = ContentScale.Fit + ) + /* Image( painter = painterResource( when (stage) { @@ -452,12 +504,13 @@ fun OpponentBattleView( .scale(-2f, 2f), // Flip horizontally contentScale = ContentScale.Fit ) + */ // Attack animation overlay if (attackAnimationProgress > 0) { - Image( - painter = painterResource(R.drawable.atk_l_00), - contentDescription = "Attack Animation", + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim000_mon03", + isLarge = true, modifier = Modifier .size(60.dp) .offset( @@ -489,6 +542,17 @@ fun BattlesScreen() { var selectedStage by remember { mutableStateOf("") } val context = LocalContext.current + + // Initialize sprite files on first load + LaunchedEffect(Unit) { + val spriteFileManager = SpriteFileManager(context) + if (!spriteFileManager.checkSpriteFilesExist()) { + println("Copying sprite files to internal storage...") + spriteFileManager.copySpriteFilesToInternalStorage() + } else { + println("Sprite files already exist in internal storage") + } + } val rookieButton = @Composable { Button( @@ -772,6 +836,7 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> + currentView = "battle-main" } } }, @@ -808,6 +873,7 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> + currentView = "battle-main" } } }, @@ -844,6 +910,7 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> + currentView = "battle-main" } } }, @@ -880,6 +947,7 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> + currentView = "battle-main" } } }, @@ -902,11 +970,74 @@ fun BattlesScreen() { } "battle-main" -> { - + var battleSystem by remember { mutableStateOf(ArenaBattleSystem()) } + var currentStage by remember { mutableStateOf("rookie") } + + // Initialize battle with character stats + LaunchedEffect(activeCharacter) { + activeCharacter?.let { character -> + battleSystem.initializeBattle( + playerHP = character.currentHp.toFloat(), + opponentHP = character.currentHp.toFloat(), + playerMaxHP = character.baseHp.toFloat(), + opponentMaxHP = character.baseHp.toFloat() + ) + } + } + + BattleScreen( + battleSystem = battleSystem, + stage = currentStage, + playerName = activeCharacter?.name ?: "Player", + opponentName = "Opponent", + activeCharacter = activeCharacter, + onBattleComplete = { winner -> + // Handle battle completion + currentView = "battle-results" + }, + onExitBattle = { + currentView = "main" + } + ) } "battle-results" -> { - + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Battle Complete!", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Results will be displayed here", + fontSize = 16.sp, + color = Color.White + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + currentView = "main" + }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Blue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text("Back to Main Menu", color = Color.White, fontSize = 16.sp) + } + } } } } From 6c9d057917ea8d5fe4f45650c7201310d6538ee8 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sat, 2 Aug 2025 07:49:43 -0400 Subject: [PATCH 10/89] Updated client for attack battle loop. --- .../vbhelper/battle/AttackSpriteManager.kt | 85 ++++++++++- .../vbhelper/battle/SpriteFileManager.kt | 33 ++-- .../vbhelper/components/AttackSpriteImage.kt | 2 + .../vbhelper/screens/BattlesScreen.kt | 143 ++++++++++++------ 4 files changed, 198 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt index 301c35f..6131c92 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -13,6 +13,20 @@ data class CharacterData( val laugeFileName: String ) +data class CharacterDataResponse( + val name: String, + val type: String, + val source_file: String, + val collection: String, + val unity_collection_id: String, + val relative_path: String, + val all_attributes: CharacterDataAttributes +) + +data class CharacterDataAttributes( + val DataList: List +) + class AttackSpriteManager(private val context: Context) { private val gson = Gson() private val characterDataCache = mutableMapOf() @@ -21,9 +35,11 @@ class AttackSpriteManager(private val context: Context) { private val attackTexturesPath = "Battle_Sprites_Reference/extracted_assets/atk_textures" fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { + println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge") try { // Get character data val characterData = getCharacterData(characterId) ?: return null + println("AttackSpriteManager: Got character data: $characterData") // Determine which attack file to use val attackFileName = if (isLarge) { @@ -31,44 +47,99 @@ class AttackSpriteManager(private val context: Context) { } else { characterData.smalefilename } + println("AttackSpriteManager: Attack filename = $attackFileName") // Skip if no attack file - if (attackFileName == "0") return null + if (attackFileName == "0") { + println("AttackSpriteManager: Skipping attack file (filename is '0')") + return null + } // Load the attack sprite val attackFilePath = "$attackTexturesPath/$attackFileName.png" val attackFile = File(context.filesDir, attackFilePath) + println("AttackSpriteManager: Attack file path = ${attackFile.absolutePath}") + println("AttackSpriteManager: Attack file exists = ${attackFile.exists()}") return if (attackFile.exists()) { - BitmapFactory.decodeFile(attackFile.absolutePath) + val bitmap = BitmapFactory.decodeFile(attackFile.absolutePath) + println("AttackSpriteManager: Successfully loaded bitmap = ${bitmap != null}") + bitmap } else { + println("AttackSpriteManager: Attack file does not exist") null } } catch (e: Exception) { + println("AttackSpriteManager: Exception occurred: ${e.message}") e.printStackTrace() return null } } private fun getCharacterData(characterId: String): CharacterData? { + println("AttackSpriteManager: Getting character data for characterId=$characterId") // Check cache first if (characterDataCache.containsKey(characterId)) { + println("AttackSpriteManager: Found character data in cache") return characterDataCache[characterId] } try { // Load character data from JSON file val characterDataFile = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats/character_data/CharacterData.json") + println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") + println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") if (!characterDataFile.exists()) { - return null + println("AttackSpriteManager: Character data file does not exist, using default data") + // For now, return a default character data + val characterData = CharacterData( + name = characterId, + charaId = characterId, + smalefilename = "atk_s_02", // Default small attack + laugeFileName = "atk_l_04" // Default large attack + ) + + characterDataCache[characterId] = characterData + return characterData } val jsonContent = characterDataFile.readText() - // Parse the JSON and find the character with matching charaId - // This is a simplified version - you'll need to parse the actual JSON structure + println("AttackSpriteManager: JSON content length = ${jsonContent.length}") - // For now, return a default character data + // Parse the JSON response + val response = gson.fromJson(jsonContent, CharacterDataResponse::class.java) + + // Search through the DataList for the matching characterId + for (characterString in response.all_attributes.DataList) { + // Extract charaId from the string format: " id=0, charaId='dim000_mon03', ...>" + val charaIdMatch = Regex("charaId='([^']+)'").find(characterString) + if (charaIdMatch != null) { + val foundCharaId = charaIdMatch.groupValues[1] + if (foundCharaId == characterId) { + // Extract smalefilename and laugeFileName + val smallFileMatch = Regex("smalefilename='([^']+)'").find(characterString) + val largeFileMatch = Regex("laugeFileName='([^']+)'").find(characterString) + + val smallFileName = smallFileMatch?.groupValues?.get(1) ?: "0" + val largeFileName = largeFileMatch?.groupValues?.get(1) ?: "0" + + val characterData = CharacterData( + name = characterId, + charaId = characterId, + smalefilename = smallFileName, + laugeFileName = largeFileName + ) + + characterDataCache[characterId] = characterData + println("AttackSpriteManager: Found character data: $characterData") + return characterData + } + } + } + + // If character not found, return default data + println("AttackSpriteManager: Character not found in JSON, using default data") val characterData = CharacterData( name = characterId, charaId = characterId, @@ -77,9 +148,11 @@ class AttackSpriteManager(private val context: Context) { ) characterDataCache[characterId] = characterData + println("AttackSpriteManager: Created default character data: $characterData") return characterData } catch (e: Exception) { + println("AttackSpriteManager: Exception in getCharacterData: ${e.message}") e.printStackTrace() return null } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index e9769b8..56f4667 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -9,16 +9,26 @@ class SpriteFileManager(private val context: Context) { fun copySpriteFilesToInternalStorage() { try { - // Create the base directory - val baseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") - if (!baseDir.exists()) { - baseDir.mkdirs() + // Create the base directory for extracted_assets + val extractedAssetsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + if (!extractedAssetsDir.exists()) { + extractedAssetsDir.mkdirs() } - // Copy files from assets to internal storage - copyAssetDirectory("Battle_Sprites_Reference/extracted_assets", baseDir) + // Create the base directory for extracted_digimon_stats + val extractedStatsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats") + if (!extractedStatsDir.exists()) { + extractedStatsDir.mkdirs() + } - println("Sprite files copied successfully to: ${baseDir.absolutePath}") + // Copy extracted_assets files from assets to internal storage + copyAssetDirectory("Battle_Sprites_Reference/extracted_assets", extractedAssetsDir) + + // Copy extracted_digimon_stats files from assets to internal storage + copyAssetDirectory("Battle_Sprites_Reference/extracted_digimon_stats", extractedStatsDir) + + println("Sprite files copied successfully to: ${extractedAssetsDir.absolutePath}") + println("Stats files copied successfully to: ${extractedStatsDir.absolutePath}") } catch (e: Exception) { println("Error copying sprite files: ${e.message}") @@ -74,7 +84,12 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val baseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") - return baseDir.exists() && baseDir.listFiles()?.isNotEmpty() == true + val extractedAssetsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + val extractedStatsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats") + + val assetsExist = extractedAssetsDir.exists() && extractedAssetsDir.listFiles()?.isNotEmpty() == true + val statsExist = extractedStatsDir.exists() && extractedStatsDir.listFiles()?.isNotEmpty() == true + + return assetsExist && statsExist } } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt index 9bf6b12..51f4ed3 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt @@ -24,11 +24,13 @@ fun AttackSpriteImage( val context = LocalContext.current LaunchedEffect(characterId, isLarge) { + println("AttackSpriteImage: Loading attack sprite for characterId=$characterId, isLarge=$isLarge") coroutineScope.launch { val attackSpriteManager = AttackSpriteManager(context) val loadedBitmap = withContext(Dispatchers.IO) { attackSpriteManager.getAttackSprite(characterId, isLarge) } + println("AttackSpriteImage: Loaded bitmap = ${loadedBitmap != null}") bitmap = loadedBitmap } } 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 d249003..50ad664 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 @@ -196,6 +196,7 @@ fun BattleScreen( playerName: String = "Player", opponentName: String = "Opponent", activeCharacter: APIBattleCharacter? = null, + opponentCharacter: APIBattleCharacter? = null, onBattleComplete: (String?) -> Unit = {}, onExitBattle: () -> Unit = {} ) { @@ -232,31 +233,52 @@ fun BattleScreen( } } + // Opponent AI - automatically attack back after player attack + LaunchedEffect(battleSystem.currentView) { + if (battleSystem.currentView == 1 && !battleSystem.isAttacking) { + // Wait a bit before opponent attacks + delay(1000) + if (battleSystem.currentView == 1 && !battleSystem.isBattleOver()) { + println("Opponent attacking back!") + battleSystem.startOpponentAttack() + // Apply damage to player + battleSystem.applyDamage(true, 15f) // Player takes damage + } + } + } + Box( modifier = Modifier .fillMaxSize() .background(Color.Black) ) { + println("Current battle view: ${battleSystem.currentView}") when (battleSystem.currentView) { - 0 -> PlayerBattleView( - battleSystem = battleSystem, - stage = stage, - playerName = playerName, - attackAnimationProgress = animationProgress, - onAttackClick = { - battleSystem.startPlayerAttack() - // Apply damage after animation - battleSystem.applyDamage(false, 20f) // Opponent takes damage - }, - activeCharacter = activeCharacter - ) - 1 -> OpponentBattleView( - battleSystem = battleSystem, - stage = stage, - opponentName = opponentName, - attackAnimationProgress = animationProgress, - activeCharacter = activeCharacter - ) + 0 -> { + println("Showing PlayerBattleView") + PlayerBattleView( + battleSystem = battleSystem, + stage = stage, + playerName = playerName, + attackAnimationProgress = animationProgress, + onAttackClick = { + battleSystem.startPlayerAttack() + // Apply damage after animation + battleSystem.applyDamage(false, 20f) // Opponent takes damage + }, + activeCharacter = activeCharacter + ) + } + 1 -> { + println("Showing OpponentBattleView") + OpponentBattleView( + battleSystem = battleSystem, + stage = stage, + opponentName = opponentName, + attackAnimationProgress = animationProgress, + activeCharacter = opponentCharacter + ) + } } // Exit button @@ -330,7 +352,7 @@ fun PlayerBattleView( ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) - atlasName = when (stage) { + atlasName = activeCharacter?.charaId ?: when (stage) { "rookie" -> "dim000_mon01" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" @@ -401,8 +423,12 @@ fun PlayerBattleView( ) // Attack button + println("PlayerBattleView: Attack button enabled = ${battleSystem.isAttackButtonEnabled}") Button( - onClick = onAttackClick, + onClick = { + println("Attack button clicked!") + onAttackClick() + }, enabled = battleSystem.isAttackButtonEnabled, modifier = Modifier .fillMaxWidth() @@ -475,7 +501,7 @@ fun OpponentBattleView( ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) - atlasName = when (stage) { + atlasName = activeCharacter?.charaId ?: when (stage) { "rookie" -> "dim000_mon01" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" @@ -537,6 +563,7 @@ fun BattlesScreen() { var opponentsList by remember { mutableStateOf(ArrayList()) } var activeCharacter by remember { mutableStateOf(null) } + var selectedOpponent by remember { mutableStateOf(null) } var expanded by remember { mutableStateOf(false) } var selectedStage by remember { mutableStateOf("") } @@ -835,6 +862,7 @@ fun BattlesScreen() { Button( onClick = { activeCharacter?.let { + selectedOpponent = opponent RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> currentView = "battle-main" } @@ -872,6 +900,7 @@ fun BattlesScreen() { Button( onClick = { activeCharacter?.let { + selectedOpponent = opponent RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> currentView = "battle-main" } @@ -909,6 +938,7 @@ fun BattlesScreen() { Button( onClick = { activeCharacter?.let { + selectedOpponent = opponent RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> currentView = "battle-main" } @@ -946,6 +976,7 @@ fun BattlesScreen() { Button( onClick = { activeCharacter?.let { + selectedOpponent = opponent RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> currentView = "battle-main" } @@ -970,35 +1001,47 @@ fun BattlesScreen() { } "battle-main" -> { - var battleSystem by remember { mutableStateOf(ArenaBattleSystem()) } - var currentStage by remember { mutableStateOf("rookie") } - - // Initialize battle with character stats - LaunchedEffect(activeCharacter) { - activeCharacter?.let { character -> - battleSystem.initializeBattle( - playerHP = character.currentHp.toFloat(), - opponentHP = character.currentHp.toFloat(), - playerMaxHP = character.baseHp.toFloat(), - opponentMaxHP = character.baseHp.toFloat() - ) - } + val battleSystem = remember { ArenaBattleSystem() } + + // Determine the current stage based on the character's stage + val currentStage = when (activeCharacter?.stage) { + 0 -> "rookie" + 1 -> "champion" + 2 -> "ultimate" + 3 -> "mega" + else -> "rookie" } - - BattleScreen( - battleSystem = battleSystem, - stage = currentStage, - playerName = activeCharacter?.name ?: "Player", - opponentName = "Opponent", - activeCharacter = activeCharacter, - onBattleComplete = { winner -> - // Handle battle completion - currentView = "battle-results" - }, - onExitBattle = { - currentView = "main" - } - ) + + // Initialize battle with character stats + LaunchedEffect(activeCharacter, selectedOpponent) { + activeCharacter?.let { playerCharacter -> + selectedOpponent?.let { opponentCharacter -> + println("Initializing battle with player: ${playerCharacter.name}, opponent: ${opponentCharacter.name}") + battleSystem.initializeBattle( + playerHP = playerCharacter.currentHp.toFloat(), + opponentHP = opponentCharacter.currentHp.toFloat(), + playerMaxHP = playerCharacter.baseHp.toFloat(), + opponentMaxHP = opponentCharacter.baseHp.toFloat() + ) + } + } + } + + BattleScreen( + battleSystem = battleSystem, + stage = currentStage, + playerName = activeCharacter?.name ?: "Player", + opponentName = selectedOpponent?.name ?: "Opponent", + activeCharacter = activeCharacter, + opponentCharacter = selectedOpponent, + onBattleComplete = { winner -> + println("Battle complete! Winner: $winner") + currentView = "battle-results" + }, + onExitBattle = { + currentView = "main" + } + ) } "battle-results" -> { From d86ee001096c14e4190d58dba698e43287d5cb34 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 11:29:23 -0400 Subject: [PATCH 11/89] Update .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d8d1c1d..7ff3a42 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ app/src/main/res/values/keys.xml app/src/test/resources/com/github/nacabaro/vbhelper/source/com.bandai.vitalbraceletarena.apk app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex + +app/src/main/java/com/github/nacabaro/vbhelper/battle/Battle_Sprites_Reference/ + +app/src/main/assets/battle_sprites \ No newline at end of file From fa8546f2830148a81cc7a61fba44dd98805f8256 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 11:29:40 -0400 Subject: [PATCH 12/89] Updated file and sprite managers to use new file structure. --- .../vbhelper/battle/AttackSpriteManager.kt | 4 +- .../vbhelper/battle/BattleSpriteManager.kt | 29 ++++------ .../vbhelper/battle/SpriteFileManager.kt | 56 +++++++++++++++++-- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt index 6131c92..d44bb43 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -32,7 +32,7 @@ class AttackSpriteManager(private val context: Context) { private val characterDataCache = mutableMapOf() // Base path for attack textures - private val attackTexturesPath = "Battle_Sprites_Reference/extracted_assets/atk_textures" + private val attackTexturesPath = "battle_sprites/extracted_assets/extracted_atksprites" fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge") @@ -86,7 +86,7 @@ class AttackSpriteManager(private val context: Context) { try { // Load character data from JSON file - val characterDataFile = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats/character_data/CharacterData.json") + val characterDataFile = File(context.filesDir, "battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 3de61f9..b4bd883 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -38,7 +38,7 @@ class BattleSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() // Base directory where your sprites are stored - private val spriteBaseDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + private val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets") fun loadSprite(spriteName: String, atlasName: String): Bitmap? { val cacheKey = "${spriteName}_${atlasName}" @@ -58,18 +58,9 @@ class BattleSpriteManager(private val context: Context) { println("Available directories: ${spriteBaseDir.listFiles()?.map { it.name }}") try { - // Load the mapping file - val mappingFile = File(spriteBaseDir, "mappings/${atlasName}_mapping.json") - if (!mappingFile.exists()) { - println("Mapping file not found: ${mappingFile.absolutePath}") - return null - } + // Load the PNG texture file directly using the atlas name + val textureFile = File(spriteBaseDir, "extracted_textures/${atlasName}.png") - val mappingJson = mappingFile.readText() - val mapping = gson.fromJson(mappingJson, SpriteMapping::class.java) - - // Load the PNG texture file - val textureFile = File(spriteBaseDir, "textures/${mapping.texture.file}") if (!textureFile.exists()) { println("Texture file not found: ${textureFile.absolutePath}") return null @@ -142,18 +133,18 @@ class BattleSpriteManager(private val context: Context) { // Helper method to get available atlases fun getAvailableAtlases(): List { try { - val mappingsDir = File(spriteBaseDir, "mappings") - if (!mappingsDir.exists()) { + val texturesDir = File(spriteBaseDir, "extracted_textures") + if (!texturesDir.exists()) { return emptyList() } - val mappingFiles = mappingsDir.listFiles { file -> - file.name.endsWith("_mapping.json") + val textureFiles = texturesDir.listFiles { file -> + file.name.endsWith(".png") } ?: emptyArray() - return mappingFiles.map { file -> - // Extract atlas name from filename (e.g., "dim000_mon01_mapping.json" -> "dim000_mon01") - file.name.substringBefore("_mapping.json") + return textureFiles.map { file -> + // Extract atlas name from filename (e.g., "dim000_mon01.png" -> "dim000_mon01") + file.name.substringBefore(".png") }.sorted() } catch (e: Exception) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 56f4667..0298f3d 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -10,22 +10,22 @@ class SpriteFileManager(private val context: Context) { fun copySpriteFilesToInternalStorage() { try { // Create the base directory for extracted_assets - val extractedAssetsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") + val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") if (!extractedAssetsDir.exists()) { extractedAssetsDir.mkdirs() } // Create the base directory for extracted_digimon_stats - val extractedStatsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats") + val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") if (!extractedStatsDir.exists()) { extractedStatsDir.mkdirs() } // Copy extracted_assets files from assets to internal storage - copyAssetDirectory("Battle_Sprites_Reference/extracted_assets", extractedAssetsDir) + copyAssetDirectory("battle_sprites/extracted_assets", extractedAssetsDir) // Copy extracted_digimon_stats files from assets to internal storage - copyAssetDirectory("Battle_Sprites_Reference/extracted_digimon_stats", extractedStatsDir) + copyAssetDirectory("battle_sprites/extracted_digimon_stats", extractedStatsDir) println("Sprite files copied successfully to: ${extractedAssetsDir.absolutePath}") println("Stats files copied successfully to: ${extractedStatsDir.absolutePath}") @@ -84,12 +84,56 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val extractedAssetsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_assets") - val extractedStatsDir = File(context.filesDir, "Battle_Sprites_Reference/extracted_digimon_stats") + val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") + val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") val assetsExist = extractedAssetsDir.exists() && extractedAssetsDir.listFiles()?.isNotEmpty() == true val statsExist = extractedStatsDir.exists() && extractedStatsDir.listFiles()?.isNotEmpty() == true return assetsExist && statsExist } + + fun clearSpriteFiles() { + try { + val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") + val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") + + if (extractedAssetsDir.exists()) { + deleteDirectory(extractedAssetsDir) + println("Cleared extracted_assets directory") + } + + if (extractedStatsDir.exists()) { + deleteDirectory(extractedStatsDir) + println("Cleared extracted_digimon_stats directory") + } + + // Also clear the battle_sprites directory if it's empty + val battleSpritesDir = File(context.filesDir, "battle_sprites") + if (battleSpritesDir.exists() && battleSpritesDir.listFiles()?.isEmpty() == true) { + battleSpritesDir.delete() + println("Cleared battle_sprites directory") + } + + } catch (e: Exception) { + println("Error clearing sprite files: ${e.message}") + e.printStackTrace() + } + } + + private fun deleteDirectory(directory: File) { + if (directory.exists()) { + val files = directory.listFiles() + if (files != null) { + for (file in files) { + if (file.isDirectory) { + deleteDirectory(file) + } else { + file.delete() + } + } + } + directory.delete() + } + } } \ No newline at end of file From a4b159da4587293f2fcce04fee38521f2414f5c2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 11:37:43 -0400 Subject: [PATCH 13/89] Sprites now load correctly for Digimon and attacks. --- .../vbhelper/battle/BattleSpriteManager.kt | 9 +++- .../vbhelper/screens/BattlesScreen.kt | 53 +++++++++++-------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index b4bd883..0af8f2b 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -82,11 +82,18 @@ class BattleSpriteManager(private val context: Context) { val spriteDataJson = spriteDataFile.readText() val spriteData = gson.fromJson(spriteDataJson, SpriteData::class.java) + // Debug: Print sprite coordinates + println("Sprite coordinates: x=${spriteData.texture_rect.x}, y=${spriteData.texture_rect.y}, width=${spriteData.texture_rect.width}, height=${spriteData.texture_rect.height}") + println("Texture dimensions: width=${fullBitmap.width}, height=${fullBitmap.height}") + + // Calculate the correct Y coordinate (inverted coordinate system) + val correctedY = fullBitmap.height - spriteData.texture_rect.y.toInt() - spriteData.texture_rect.height.toInt() + // Extract the sprite from the atlas using texture_rect coordinates val spriteBitmap = Bitmap.createBitmap( fullBitmap, spriteData.texture_rect.x.toInt(), - spriteData.texture_rect.y.toInt(), + correctedY, spriteData.texture_rect.width.toInt(), spriteData.texture_rect.height.toInt() ) 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 50ad664..8bcdf1a 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 @@ -252,10 +252,10 @@ fun BattleScreen( .fillMaxSize() .background(Color.Black) ) { - println("Current battle view: ${battleSystem.currentView}") + //println("Current battle view: ${battleSystem.currentView}") when (battleSystem.currentView) { 0 -> { - println("Showing PlayerBattleView") + //println("Showing PlayerBattleView") PlayerBattleView( battleSystem = battleSystem, stage = stage, @@ -270,8 +270,8 @@ fun BattleScreen( ) } 1 -> { - println("Showing OpponentBattleView") - OpponentBattleView( + //println("Showing OpponentBattleView") + OpponentBattleView( battleSystem = battleSystem, stage = stage, opponentName = opponentName, @@ -353,11 +353,11 @@ fun PlayerBattleView( SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim000_mon01" + "rookie" -> "dim275_mon01" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" "mega" -> "dim012_mon14" - else -> "dim000_mon01" + else -> "dim275_mon01" }, modifier = Modifier .size(120.dp) @@ -385,17 +385,17 @@ fun PlayerBattleView( // Attack animation overlay if (attackAnimationProgress > 0) { - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim000_mon03", - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = (attackAnimationProgress * 200 - 100).dp, - y = 0.dp - ), - contentScale = ContentScale.Fit - ) + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim275_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = (attackAnimationProgress * 200 - 100).dp, + y = 0.dp + ), + contentScale = ContentScale.Fit + ) /* Image( painter = painterResource(R.drawable.atk_l_00), @@ -423,7 +423,7 @@ fun PlayerBattleView( ) // Attack button - println("PlayerBattleView: Attack button enabled = ${battleSystem.isAttackButtonEnabled}") + //println("PlayerBattleView: Attack button enabled = ${battleSystem.isAttackButtonEnabled}") Button( onClick = { println("Attack button clicked!") @@ -502,11 +502,11 @@ fun OpponentBattleView( SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim000_mon01" + "rookie" -> "dim275_mon01" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" "mega" -> "dim012_mon14" - else -> "dim000_mon01" + else -> "dim275_mon01" }, modifier = Modifier .size(120.dp) @@ -535,7 +535,7 @@ fun OpponentBattleView( // Attack animation overlay if (attackAnimationProgress > 0) { AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim000_mon03", + characterId = activeCharacter?.charaId ?: "dim275_mon01", isLarge = true, modifier = Modifier .size(60.dp) @@ -759,7 +759,7 @@ fun BattlesScreen() { // Create hardcoded character lists for each stage val rookieCharacters = listOf( APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), - APIBattleCharacter("PULSEMON", "degimon_name_Dim000_003", "dim000_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("PULSEMON", "degimon_name_Dim275_001", "dim275_mon01", 0, 1, 1800, 1800, 2400.0f, 700.0f), APIBattleCharacter("DORUMON", "degimon_name_dim137_mon03", "dim137_mon03", 0, 1, 3000, 3000, 5100.0f, 1050.0f) ) @@ -844,6 +844,15 @@ fun BattlesScreen() { championButton() ultimateButton() megaButton() + Button( + onClick = { + val spriteFileManager = SpriteFileManager(context) + spriteFileManager.clearSpriteFiles() + println("Sprite files cleared!") + } + ) { + Text("Clear Sprite Files") + } } } From 266658342ab72686122d7087a2ff9edd17bea4ff Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 12:14:24 -0400 Subject: [PATCH 14/89] Fixed sprite sizing. --- .../vbhelper/battle/BattleSpriteManager.kt | 20 +++++++++++++--- .../vbhelper/screens/BattlesScreen.kt | 24 +++++++++---------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 0af8f2b..05d0e34 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -98,10 +98,24 @@ class BattleSpriteManager(private val context: Context) { spriteData.texture_rect.height.toInt() ) - // Cache the result - spriteCache[cacheKey] = spriteBitmap + // Ensure the bitmap is not scaled and has proper quality + val finalBitmap = if (spriteBitmap.width != spriteData.texture_rect.width.toInt() || + spriteBitmap.height != spriteData.texture_rect.height.toInt()) { + // If the bitmap was scaled during creation, create a new one with exact dimensions + Bitmap.createScaledBitmap(spriteBitmap, + spriteData.texture_rect.width.toInt(), + spriteData.texture_rect.height.toInt(), + false) // false = no filtering/interpolation + } else { + spriteBitmap + } - return spriteBitmap + println("Extracted sprite dimensions: ${finalBitmap.width}x${finalBitmap.height}") + + // Cache the result + spriteCache[cacheKey] = finalBitmap + + return finalBitmap } catch (e: Exception) { e.printStackTrace() 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 8bcdf1a..df8dbae 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 @@ -347,21 +347,20 @@ fun PlayerBattleView( // Player character with attack animation Box( - modifier = Modifier.size(120.dp), + modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim275_mon01" + "rookie" -> "dim000_mon03" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" "mega" -> "dim012_mon14" - else -> "dim275_mon01" + else -> "dim011_mon01" }, modifier = Modifier - .size(120.dp) - .scale(2f), + .size(80.dp), contentScale = ContentScale.Fit ) /* @@ -386,7 +385,7 @@ fun PlayerBattleView( // Attack animation overlay if (attackAnimationProgress > 0) { AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim275_mon01", + characterId = activeCharacter?.charaId ?: "dim011_mon01", isLarge = true, modifier = Modifier .size(60.dp) @@ -496,21 +495,20 @@ fun OpponentBattleView( // Opponent character with attack animation Box( - modifier = Modifier.size(120.dp), + modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim275_mon01" + "rookie" -> "dim000_mon03" "champion" -> "dim012_mon04" "ultimate" -> "dim137_mon09" "mega" -> "dim012_mon14" - else -> "dim275_mon01" + else -> "dim011_mon01" }, modifier = Modifier - .size(120.dp) - .scale(2f), + .size(80.dp), contentScale = ContentScale.Fit ) /* @@ -535,7 +533,7 @@ fun OpponentBattleView( // Attack animation overlay if (attackAnimationProgress > 0) { AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim275_mon01", + characterId = activeCharacter?.charaId ?: "dim011_mon01", isLarge = true, modifier = Modifier .size(60.dp) @@ -759,7 +757,7 @@ fun BattlesScreen() { // Create hardcoded character lists for each stage val rookieCharacters = listOf( APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), - APIBattleCharacter("PULSEMON", "degimon_name_Dim275_001", "dim275_mon01", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("PULSEMON", "degimon_name_Dim000_003", "dim000_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), APIBattleCharacter("DORUMON", "degimon_name_dim137_mon03", "dim137_mon03", 0, 1, 3000, 3000, 5100.0f, 1050.0f) ) From 2901bcf0da44018a2471ae67c55664ac7200f200 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 12:39:03 -0400 Subject: [PATCH 15/89] Updated attack button to actually perform stage 1 API call. --- .../vbhelper/screens/BattlesScreen.kt | 182 +++++++++++------- 1 file changed, 116 insertions(+), 66 deletions(-) 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 df8dbae..ac2cfd5 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 @@ -198,7 +198,8 @@ fun BattleScreen( activeCharacter: APIBattleCharacter? = null, opponentCharacter: APIBattleCharacter? = null, onBattleComplete: (String?) -> Unit = {}, - onExitBattle: () -> Unit = {} + onExitBattle: () -> Unit = {}, + context: android.content.Context? = null ) { var animationProgress by remember { mutableStateOf(0f) } @@ -266,7 +267,9 @@ fun BattleScreen( // Apply damage after animation battleSystem.applyDamage(false, 20f) // Opponent takes damage }, - activeCharacter = activeCharacter + activeCharacter = activeCharacter, + context = context, + opponent = opponentCharacter ) } 1 -> { @@ -301,7 +304,9 @@ fun PlayerBattleView( playerName: String, attackAnimationProgress: Float, onAttackClick: () -> Unit, - activeCharacter: APIBattleCharacter? = null + activeCharacter: APIBattleCharacter? = null, + context: android.content.Context? = null, + opponent: APIBattleCharacter? = null ) { Column( modifier = Modifier @@ -347,8 +352,10 @@ fun PlayerBattleView( // Player character with attack animation Box( - modifier = Modifier.size(80.dp), - contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxWidth() + .size(80.dp), + contentAlignment = Alignment.CenterStart ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) @@ -360,7 +367,8 @@ fun PlayerBattleView( else -> "dim011_mon01" }, modifier = Modifier - .size(80.dp), + .size(80.dp) + .scale(-1f, 1f), // Flip horizontally contentScale = ContentScale.Fit ) /* @@ -426,6 +434,45 @@ fun PlayerBattleView( Button( onClick = { println("Attack button clicked!") + + // Get crit bar progress as float (0.0f to 100.0f) + val critBarProgressFloat = battleSystem.critBarProgress.toFloat() + + // Determine player and opponent stages + val playerStage = when (activeCharacter?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + val opponentStage = when (opponent?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + // Send API call with all parameters + context?.let { ctx -> + RetrofitHelper().getPVPWinner( + ctx, + 1, + 2, + activeCharacter?.name ?: "Player", + playerStage, + opponentStage, + opponent?.name ?: "Opponent", + opponentStage + ) { apiResult -> + // Handle API response here + println("API Result: $apiResult") + // TODO: Parse response and apply damage/hit/miss logic + } + } + onAttackClick() }, enabled = battleSystem.isAttackButtonEnabled, @@ -495,8 +542,10 @@ fun OpponentBattleView( // Opponent character with attack animation Box( - modifier = Modifier.size(80.dp), - contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxWidth() + .size(80.dp), + contentAlignment = Alignment.CenterEnd ) { SpriteImage( spriteName = "00", // The sprite number (00, 01, 02, etc.) @@ -856,41 +905,41 @@ fun BattlesScreen() { "rookie" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Rookie Battle View") + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Rookie Battle View") - // Add character selection dropdown - characterDropdown("rookie") + // Add character selection dropdown + characterDropdown("rookie") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> - currentView = "battle-main" + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> + currentView = "battle-main" + } } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } } - } - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}") - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") - } + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } - backButton() - } + backButton() + } } "champion" -> { @@ -1019,36 +1068,37 @@ fun BattlesScreen() { else -> "rookie" } - // Initialize battle with character stats - LaunchedEffect(activeCharacter, selectedOpponent) { - activeCharacter?.let { playerCharacter -> - selectedOpponent?.let { opponentCharacter -> - println("Initializing battle with player: ${playerCharacter.name}, opponent: ${opponentCharacter.name}") - battleSystem.initializeBattle( - playerHP = playerCharacter.currentHp.toFloat(), - opponentHP = opponentCharacter.currentHp.toFloat(), - playerMaxHP = playerCharacter.baseHp.toFloat(), - opponentMaxHP = opponentCharacter.baseHp.toFloat() - ) - } - } - } + // Initialize battle with character stats + LaunchedEffect(activeCharacter, selectedOpponent) { + activeCharacter?.let { playerCharacter -> + selectedOpponent?.let { opponentCharacter -> + println("Initializing battle with player: ${playerCharacter.name}, opponent: ${opponentCharacter.name}") + battleSystem.initializeBattle( + playerHP = playerCharacter.currentHp.toFloat(), + opponentHP = opponentCharacter.currentHp.toFloat(), + playerMaxHP = playerCharacter.baseHp.toFloat(), + opponentMaxHP = opponentCharacter.baseHp.toFloat() + ) + } + } + } - BattleScreen( - battleSystem = battleSystem, - stage = currentStage, - playerName = activeCharacter?.name ?: "Player", - opponentName = selectedOpponent?.name ?: "Opponent", - activeCharacter = activeCharacter, - opponentCharacter = selectedOpponent, - onBattleComplete = { winner -> - println("Battle complete! Winner: $winner") - currentView = "battle-results" - }, - onExitBattle = { - currentView = "main" - } - ) + BattleScreen( + battleSystem = battleSystem, + stage = currentStage, + playerName = activeCharacter?.name ?: "Player", + opponentName = selectedOpponent?.name ?: "Opponent", + activeCharacter = activeCharacter, + opponentCharacter = selectedOpponent, + onBattleComplete = { winner -> + println("Battle complete! Winner: $winner") + currentView = "battle-results" + }, + onExitBattle = { + currentView = "main" + }, + context = context + ) } "battle-results" -> { From 023af17b230df2c71387e32667d152d7214e1d72 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 13:23:46 -0400 Subject: [PATCH 16/89] Updated BattleScreen to perform full battle loop with API calls. --- .../vbhelper/screens/BattlesScreen.kt | 157 +++++++++++++++++- 1 file changed, 148 insertions(+), 9 deletions(-) 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 ac2cfd5..74aec30 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 @@ -152,6 +152,13 @@ class ArenaBattleSystem { _critBarProgress = progress } + // Update HP from API response + fun updateHPFromAPI(playerHP: Float, opponentHP: Float) { + _playerCurrentHP = playerHP.coerceAtLeast(0f) + _opponentCurrentHP = opponentHP.coerceAtLeast(0f) + Log.d(TAG, "HP updated from API: Player HP $playerHP, Opponent HP $opponentHP") + } + //Check if battle is over fun isBattleOver(): Boolean { @@ -187,6 +194,18 @@ class ArenaBattleSystem { _attackProgress = 0f Log.d(TAG, "Battle system cleaned up") } + + // Enable attack button + fun enableAttackButton() { + _isAttackButtonEnabled = true + Log.d(TAG, "Attack button enabled") + } + + // Disable attack button + fun disableAttackButton() { + _isAttackButtonEnabled = false + Log.d(TAG, "Attack button disabled") + } } @Composable @@ -288,7 +307,7 @@ fun BattleScreen( Button( onClick = onExitBattle, modifier = Modifier - .align(Alignment.TopEnd) + .align(Alignment.TopCenter) .padding(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Color.Red) ) { @@ -457,6 +476,9 @@ fun PlayerBattleView( // Send API call with all parameters context?.let { ctx -> + // Start player attack animation + battleSystem.startPlayerAttack() + RetrofitHelper().getPVPWinner( ctx, 1, @@ -469,11 +491,66 @@ fun PlayerBattleView( ) { apiResult -> // Handle API response here println("API Result: $apiResult") - // TODO: Parse response and apply damage/hit/miss logic + + // Update HP based on API response + when (apiResult.state) { + 1 -> { + // Match is still ongoing - update HP and continue + println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") + + // Apply the actual damage from API + if (apiResult.playerAttackHit) { + val playerDamage = apiResult.playerAttackDamage.toFloat() + if (playerDamage > 0) { + battleSystem.applyDamage(false, playerDamage) // Opponent takes damage + println("Player attack hit! Damage: $playerDamage") + } + } else { + println("Player attack missed!") + } + + if (apiResult.opponentAttackDamage > 0) { + val opponentDamage = apiResult.opponentAttackDamage.toFloat() + battleSystem.applyDamage(true, opponentDamage) // Player takes damage + println("Opponent attack hit! Damage: $opponentDamage") + } + + // Update HP to match API response + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + + // Keep attack button enabled for next round + battleSystem.enableAttackButton() + } + 2 -> { + // Match is over - report winner and complete battle + println("Match over! Winner: ${apiResult.winner}") + println("Final HP - Player: ${apiResult.playerHP}, Opponent: ${apiResult.opponentHP}") + + // Update final HP + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + + // Disable attack button since match is over + battleSystem.disableAttackButton() + + // Complete the battle + onAttackClick() + } + -1 -> { + // Error state + println("API Error: ${apiResult.status}") + // Re-enable attack button on error + battleSystem.enableAttackButton() + } + else -> { + println("Unknown state: ${apiResult.state}") + // Re-enable attack button on unknown state + battleSystem.enableAttackButton() + } + } } } - onAttackClick() + // Don't call onAttackClick() here - only call it when match is over (state = 2) }, enabled = battleSystem.isAttackButtonEnabled, modifier = Modifier @@ -1102,6 +1179,59 @@ fun BattlesScreen() { } "battle-results" -> { + var winnerName by remember { mutableStateOf("") } + var isWinnerLoaded by remember { mutableStateOf(false) } + + // Send one more stage 1 call to get winner info, then cleanup + LaunchedEffect(Unit) { + // Determine player and opponent stages + val playerStage = when (activeCharacter?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + val opponentStage = when (selectedOpponent?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + // First, send one more stage 1 call to get winner info + RetrofitHelper().getPVPWinner( + context, + 1, + 2, + activeCharacter?.name ?: "Player", + playerStage, + opponentStage, + selectedOpponent?.name ?: "Opponent", + opponentStage + ) { winnerResult -> + println("Winner API Result: $winnerResult") + winnerName = winnerResult.winner + isWinnerLoaded = true + + // Now send cleanup call + RetrofitHelper().getPVPWinner( + context, + 2, + 2, + activeCharacter?.name ?: "Player", + playerStage, + opponentStage, + selectedOpponent?.name ?: "Opponent", + opponentStage + ) { cleanupResult -> + println("Cleanup API Result: $cleanupResult") + } + } + } + Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center @@ -1110,16 +1240,25 @@ fun BattlesScreen() { text = "Battle Complete!", fontSize = 24.sp, fontWeight = FontWeight.Bold, - color = Color.White + color = Color.Gray ) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Results will be displayed here", - fontSize = 16.sp, - color = Color.White - ) + if (isWinnerLoaded) { + Text( + text = "Winner: $winnerName", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray + ) + } else { + Text( + text = "Loading results...", + fontSize = 16.sp, + color = Color.Gray + ) + } Spacer(modifier = Modifier.height(32.dp)) From 3b762d6195c7fbff8efcc43fe9b25dbf04301d06 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 3 Aug 2025 15:25:37 -0400 Subject: [PATCH 17/89] Updated battle loop. Started to fix attack animations. --- .../vbhelper/screens/BattlesScreen.kt | 340 +++++++++++------- 1 file changed, 215 insertions(+), 125 deletions(-) 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 74aec30..53814cc 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 @@ -10,14 +10,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.Image +//import androidx.compose.foundation.Image import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource +//import androidx.compose.ui.res.painterResource import androidx.compose.material3.Button import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -70,6 +70,10 @@ class ArenaBattleSystem { private var _isAttackVisible by mutableStateOf(false) private var _critBarProgress by mutableStateOf(0) private var _isAttackButtonEnabled by mutableStateOf(true) + + // Attack animation state + private var _attackIsHit by mutableStateOf(false) + private var _isPlayerAttacking by mutableStateOf(false) // Exposed state for Compose val playerCurrentHP: Float get() = _playerCurrentHP @@ -82,6 +86,10 @@ class ArenaBattleSystem { val isAttackVisible: Boolean get() = _isAttackVisible val critBarProgress: Int get() = _critBarProgress val isAttackButtonEnabled: Boolean get() = _isAttackButtonEnabled + + // Attack animation state + val attackIsHit: Boolean get() = _attackIsHit + val isPlayerAttacking: Boolean get() = _isPlayerAttacking //Initialize battle with character data fun initializeBattle( @@ -110,6 +118,8 @@ class ArenaBattleSystem { _currentView = 0 _isAttackVisible = true _isAttackButtonEnabled = false + _isPlayerAttacking = true + _attackProgress = 0f Log.d(TAG, "Player attack started") } @@ -118,6 +128,8 @@ class ArenaBattleSystem { _isAttacking = true _currentView = 1 _isAttackVisible = true + _isPlayerAttacking = false + _attackProgress = 0f Log.d(TAG, "Opponent attack started") } @@ -127,12 +139,23 @@ class ArenaBattleSystem { } //Complete attack animation - fun completeAttackAnimation() { + fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { _isAttacking = false _isAttackVisible = false _attackProgress = 0f - _currentView = if (_currentView == 0) 1 else 0 - _isAttackButtonEnabled = true + _attackIsHit = false + + // Apply damage at end of animation + if (playerDamage > 0) { + applyDamage(true, playerDamage) // Player takes damage + println("Player took $playerDamage damage at end of animation") + } + if (opponentDamage > 0) { + applyDamage(false, opponentDamage) // Opponent takes damage + println("Opponent took $opponentDamage damage at end of animation") + } + + // Don't enable attack button here - it will be enabled when we switch back to player view Log.d(TAG, "Attack animation completed") } @@ -206,6 +229,18 @@ class ArenaBattleSystem { _isAttackButtonEnabled = false Log.d(TAG, "Attack button disabled") } + + // Set attack hit/miss state + fun setAttackHitState(isHit: Boolean) { + _attackIsHit = isHit + Log.d(TAG, "Attack hit state set to: $isHit") + } + + // Switch to specific view + fun switchToView(view: Int) { + _currentView = view + Log.d(TAG, "Switched to view: $view") + } } @Composable @@ -221,6 +256,9 @@ fun BattleScreen( context: android.content.Context? = null ) { var animationProgress by remember { mutableStateOf(0f) } + var pendingPlayerDamage by remember { mutableStateOf(0f) } + var pendingOpponentDamage by remember { mutableStateOf(0f) } + var currentAttackType by remember { mutableStateOf("") } // Track if this is player or enemy attack // Critical bar timer LaunchedEffect(Unit) { @@ -232,10 +270,19 @@ fun BattleScreen( } } - // Attack animation - LaunchedEffect(battleSystem.isAttacking) { - if (battleSystem.isAttacking) { + // Player attack animation + LaunchedEffect(battleSystem.isAttacking, battleSystem.isPlayerAttacking) { + if (battleSystem.isAttacking && battleSystem.isPlayerAttacking) { animationProgress = 0f + println("=== Starting PLAYER attack animation ===") + println("Current view: ${battleSystem.currentView}") + println("Is player attacking: ${battleSystem.isPlayerAttacking}") + + // Store the current attack type + currentAttackType = "player" + println("Attack type: $currentAttackType") + + // Complete attack animation across the full screen animate( initialValue = 0f, targetValue = 1f, @@ -243,26 +290,83 @@ fun BattleScreen( ) { value, _ -> animationProgress = value battleSystem.updateAttackAnimation(value) + println("Animation progress: $value") } - battleSystem.completeAttackAnimation() - + + println("=== PLAYER Animation completed ===") + println("Final animation progress: $animationProgress") + + // Complete the attack animation + battleSystem.completeAttackAnimation(pendingPlayerDamage, pendingOpponentDamage) + + // Reset pending damage + pendingPlayerDamage = 0f + pendingOpponentDamage = 0f + + // Player attack just completed - switch to enemy view + println("Player attack completed, switching to enemy view") + battleSystem.switchToView(1) + // Check if battle is over if (battleSystem.isBattleOver()) { onBattleComplete(battleSystem.getWinner()) } } } - - // Opponent AI - automatically attack back after player attack - LaunchedEffect(battleSystem.currentView) { - if (battleSystem.currentView == 1 && !battleSystem.isAttacking) { - // Wait a bit before opponent attacks - delay(1000) - if (battleSystem.currentView == 1 && !battleSystem.isBattleOver()) { - println("Opponent attacking back!") - battleSystem.startOpponentAttack() - // Apply damage to player - battleSystem.applyDamage(true, 15f) // Player takes damage + + // Enemy attack trigger (separate from animation) + LaunchedEffect(currentAttackType) { + if (currentAttackType == "player") { + // Player attack just completed - trigger enemy attack after delay + delay(500) // Wait before enemy attacks back + println("Starting enemy attack") + battleSystem.startOpponentAttack() + println("Enemy attack triggered - LaunchedEffect should re-trigger") + } + } + + // Enemy attack animation + LaunchedEffect(battleSystem.isAttacking, battleSystem.isPlayerAttacking) { + if (battleSystem.isAttacking && !battleSystem.isPlayerAttacking) { + animationProgress = 0f + println("=== Starting ENEMY attack animation ===") + println("Current view: ${battleSystem.currentView}") + println("Is player attacking: ${battleSystem.isPlayerAttacking}") + + // Store the current attack type + currentAttackType = "enemy" + println("Attack type: $currentAttackType") + + // Complete attack animation across the full screen + animate( + initialValue = 0f, + targetValue = 1f, + animationSpec = tween(ArenaBattleSystem.ANIMATION_DURATION.toInt()) + ) { value, _ -> + animationProgress = value + battleSystem.updateAttackAnimation(value) + println("Animation progress: $value") + } + + println("=== ENEMY Animation completed ===") + println("Final animation progress: $animationProgress") + + // Complete the attack animation + battleSystem.completeAttackAnimation(pendingPlayerDamage, pendingOpponentDamage) + + // Reset pending damage + pendingPlayerDamage = 0f + pendingOpponentDamage = 0f + + // Enemy attack just completed - switch back to player view + println("Enemy attack completed, switching back to player view") + battleSystem.switchToView(0) + // Enable attack button when we're back on player view + battleSystem.enableAttackButton() + + // Check if battle is over + if (battleSystem.isBattleOver()) { + onBattleComplete(battleSystem.getWinner()) } } } @@ -283,12 +387,15 @@ fun BattleScreen( attackAnimationProgress = animationProgress, onAttackClick = { battleSystem.startPlayerAttack() - // Apply damage after animation - battleSystem.applyDamage(false, 20f) // Opponent takes damage + // Damage will be applied at end of animation via pending damage system }, activeCharacter = activeCharacter, context = context, - opponent = opponentCharacter + opponent = opponentCharacter, + onSetPendingDamage = { playerDamage, opponentDamage -> + pendingPlayerDamage = playerDamage + pendingOpponentDamage = opponentDamage + } ) } 1 -> { @@ -325,7 +432,8 @@ fun PlayerBattleView( onAttackClick: () -> Unit, activeCharacter: APIBattleCharacter? = null, context: android.content.Context? = null, - opponent: APIBattleCharacter? = null + opponent: APIBattleCharacter? = null, + onSetPendingDamage: (Float, Float) -> Unit ) { Column( modifier = Modifier @@ -390,51 +498,37 @@ fun PlayerBattleView( .scale(-1f, 1f), // Flip horizontally contentScale = ContentScale.Fit ) - /* - Image( - painter = painterResource( - when (stage) { - "rookie" -> R.drawable.agumon - "champion" -> R.drawable.greymon - "ultimate" -> R.drawable.doruguremon - "mega" -> R.drawable.machinedramon - else -> R.drawable.agumon - } - ), - contentDescription = "Player Character", - modifier = Modifier - .size(120.dp) - .scale(2f), - contentScale = ContentScale.Fit - ) - */ // Attack animation overlay if (attackAnimationProgress > 0) { - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = (attackAnimationProgress * 200 - 100).dp, - y = 0.dp - ), - contentScale = ContentScale.Fit - ) - /* - Image( - painter = painterResource(R.drawable.atk_l_00), - contentDescription = "Attack Animation", - modifier = Modifier - .size(60.dp) - .offset( - x = (attackAnimationProgress * 200 - 100).dp, - y = 0.dp - ), - contentScale = ContentScale.Fit - ) - */ + // Show player attack on both player and enemy screens + // Show enemy attack on both enemy and player screens + val shouldShowAttack = true // Always show attack during animation + + if (shouldShowAttack) { + val xOffset = if (battleSystem.isPlayerAttacking) { + // Player attack: start from player position (left side) and fly to right edge + (attackAnimationProgress * 400 - 200).dp + } else { + // Enemy attack: start from right edge and fly to left edge + (-attackAnimationProgress * 400 + 200).dp + } + + println("Attack sprite - Progress: $attackAnimationProgress, IsPlayerAttacking: ${battleSystem.isPlayerAttacking}, X Offset: $xOffset") + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks + contentScale = ContentScale.Fit + ) + } } } @@ -494,40 +588,37 @@ fun PlayerBattleView( // Update HP based on API response when (apiResult.state) { - 1 -> { - // Match is still ongoing - update HP and continue - println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - - // Apply the actual damage from API - if (apiResult.playerAttackHit) { - val playerDamage = apiResult.playerAttackDamage.toFloat() - if (playerDamage > 0) { - battleSystem.applyDamage(false, playerDamage) // Opponent takes damage - println("Player attack hit! Damage: $playerDamage") - } - } else { - println("Player attack missed!") - } - - if (apiResult.opponentAttackDamage > 0) { - val opponentDamage = apiResult.opponentAttackDamage.toFloat() - battleSystem.applyDamage(true, opponentDamage) // Player takes damage - println("Opponent attack hit! Damage: $opponentDamage") - } - - // Update HP to match API response - battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) - - // Keep attack button enabled for next round - battleSystem.enableAttackButton() - } + 1 -> { + // Match is still ongoing - update HP and continue + println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") + + // Handle damage timing based on hit/miss + if (apiResult.playerAttackHit) { + // Player attack hit - enemy takes damage at end of player animation + // Player will dodge enemy attack + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - player takes damage at end of enemy animation + println("Player attack missed! Player will take damage from enemy attack") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + battleSystem.setAttackHitState(false) + } + + // Don't update HP immediately - wait for animation to complete + // battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + + // Keep attack button enabled for next round + battleSystem.enableAttackButton() + } 2 -> { // Match is over - report winner and complete battle println("Match over! Winner: ${apiResult.winner}") println("Final HP - Player: ${apiResult.playerHP}, Opponent: ${apiResult.opponentHP}") - // Update final HP - battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + // Don't update HP immediately - let animation complete first + // battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) // Disable attack button since match is over battleSystem.disableAttackButton() @@ -637,38 +728,37 @@ fun OpponentBattleView( .size(80.dp), contentScale = ContentScale.Fit ) - /* - Image( - painter = painterResource( - when (stage) { - "rookie" -> R.drawable.agumon - "champion" -> R.drawable.greymon - "ultimate" -> R.drawable.doruguremon - "mega" -> R.drawable.machinedramon - else -> R.drawable.agumon - } - ), - contentDescription = "Opponent Character", - modifier = Modifier - .size(120.dp) - .scale(-2f, 2f), // Flip horizontally - contentScale = ContentScale.Fit - ) - */ // Attack animation overlay if (attackAnimationProgress > 0) { - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = (-attackAnimationProgress * 200 + 100).dp, - y = 0.dp - ), - contentScale = ContentScale.Fit - ) + // Show player attack on both player and enemy screens + // Show enemy attack on both enemy and player screens + val shouldShowAttack = true // Always show attack during animation + + if (shouldShowAttack) { + val xOffset = if (battleSystem.isPlayerAttacking) { + // Player attack: start from left edge and fly to right edge + (attackAnimationProgress * 400 - 200).dp + } else { + // Enemy attack: start from enemy position (right side) and fly to left edge + (-attackAnimationProgress * 400 + 200).dp + } + + println("OpponentBattleView - Attack sprite - Progress: $attackAnimationProgress, IsPlayerAttacking: ${battleSystem.isPlayerAttacking}, X Offset: $xOffset") + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks + contentScale = ContentScale.Fit + ) + } } } From 942d8436011466ba2ff63d93ae8ef2c07450df0f Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 10:04:05 -0400 Subject: [PATCH 18/89] Create ArenaBattleSystem.kt --- .../vbhelper/battle/ArenaBattleSystem.kt | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt 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 new file mode 100644 index 0000000..2c3fc37 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt @@ -0,0 +1,134 @@ +package com.github.nacabaro.vbhelper.battle + +import android.util.Log +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue + +class ArenaBattleSystem { + companion object { + private const val TAG = "ArenaBattleSystem" + } + + // Attack phases: 0=Idle, 1=Player attack on player screen, 2=Player attack on opponent screen, + // 3=Opponent attack on opponent screen, 4=Opponent attack on player screen + private var _attackPhase by mutableStateOf(0) + val attackPhase: Int get() = _attackPhase + + private var _attackProgress by mutableStateOf(0f) + val attackProgress: Float get() = _attackProgress + + private var _isPlayerAttacking by mutableStateOf(false) + val isPlayerAttacking: Boolean get() = _isPlayerAttacking + + private var _attackIsHit by mutableStateOf(false) + val attackIsHit: Boolean get() = _attackIsHit + + private var _isAttackButtonEnabled by mutableStateOf(true) + val isAttackButtonEnabled: Boolean get() = _isAttackButtonEnabled + + private var _currentView by mutableStateOf(0) + val currentView: Int get() = _currentView + + private var _playerHP by mutableStateOf(100f) + val playerHP: Float get() = _playerHP + + private var _opponentHP by mutableStateOf(100f) + val opponentHP: Float get() = _opponentHP + + private var _isBattleOver by mutableStateOf(false) + val isBattleOver: Boolean get() = _isBattleOver + + private var _critBarProgress by mutableStateOf(0) + val critBarProgress: Int get() = _critBarProgress + + fun startPlayerAttack() { + Log.d(TAG, "Starting player attack") + _attackPhase = 1 + _attackProgress = 0f + _isPlayerAttacking = true + _isAttackButtonEnabled = false + _currentView = 0 + } + + fun startOpponentAttack() { + Log.d(TAG, "Starting opponent attack") + _attackPhase = 3 + _attackProgress = 0f + _isPlayerAttacking = false + _currentView = 1 + } + + fun advanceAttackPhase() { + _attackPhase++ + _attackProgress = 0f + Log.d(TAG, "Advanced to attack phase: $_attackPhase") + } + + fun setAttackProgress(progress: Float) { + _attackProgress = progress + } + + fun setAttackHitState(isHit: Boolean) { + _attackIsHit = isHit + } + + fun switchToView(view: Int) { + _currentView = view + Log.d(TAG, "Switched to view: $view") + } + + fun enableAttackButton() { + _isAttackButtonEnabled = true + Log.d(TAG, "Attack button enabled") + } + + fun applyDamage(isPlayer: Boolean, damage: Float) { + if (isPlayer) { + _playerHP = (_playerHP - damage).coerceAtLeast(0f) + } else { + _opponentHP = (_opponentHP - damage).coerceAtLeast(0f) + } + Log.d(TAG, "Applied damage: ${if (isPlayer) "player" else "opponent"} -$damage") + } + + fun updateHPFromAPI(playerHP: Float, opponentHP: Float) { + _playerHP = playerHP + _opponentHP = opponentHP + Log.d(TAG, "Updated HP from API: Player=$playerHP, Opponent=$opponentHP") + } + + fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { + if (playerDamage > 0f) { + applyDamage(true, playerDamage) + } + if (opponentDamage > 0f) { + applyDamage(false, opponentDamage) + } + Log.d(TAG, "Completed attack animation with damage: Player=$playerDamage, Opponent=$opponentDamage") + } + + fun resetAttackState() { + _attackPhase = 0 + _attackProgress = 0f + _isPlayerAttacking = false + _attackIsHit = false + _currentView = 0 + Log.d(TAG, "Reset attack state") + } + + fun checkBattleOver(): Boolean { + return _playerHP <= 0f || _opponentHP <= 0f + } + + fun endBattle() { + _isBattleOver = true + Log.d(TAG, "Battle ended") + } + + fun updateCritBarProgress(progress: Int) { + _critBarProgress = progress + Log.d(TAG, "Updated crit bar progress: $progress") + } +} \ No newline at end of file From d7b10b1ae8d261d01e43abb03cb63f210a7cfb8d Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 10:04:30 -0400 Subject: [PATCH 19/89] Update BattlesScreen.kt --- .../vbhelper/screens/BattlesScreen.kt | 1103 +++++------------ 1 file changed, 315 insertions(+), 788 deletions(-) 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 53814cc..b354253 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 @@ -10,14 +10,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.Spacer -//import androidx.compose.foundation.Image import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -//import androidx.compose.ui.res.painterResource import androidx.compose.material3.Button import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,358 +34,133 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.Box import androidx.compose.ui.platform.LocalContext import androidx.compose.runtime.LaunchedEffect -import androidx.compose.animation.core.animate +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -//import androidx.compose.animation.core.animateFloatAsState import kotlinx.coroutines.delay import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import com.github.nacabaro.vbhelper.battle.APIBattleCharacter -//import com.github.nacabaro.vbhelper.battle.BattleSpriteManager import android.util.Log import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.battle.RetrofitHelper import com.github.nacabaro.vbhelper.battle.SpriteImage import com.github.nacabaro.vbhelper.battle.AttackSpriteImage import com.github.nacabaro.vbhelper.battle.SpriteFileManager - -class ArenaBattleSystem { - companion object { - private const val TAG = "VBArenaBattleSystem" - const val ANIMATION_DURATION = 1500L - } - - // Battle state - private var _playerCurrentHP by mutableStateOf(100f) - private var _playerMaxHP by mutableStateOf(100f) - private var _opponentCurrentHP by mutableStateOf(100f) - private var _opponentMaxHP by mutableStateOf(100f) - private var _isAttacking by mutableStateOf(false) - private var _attackProgress by mutableStateOf(0f) - private var _currentView by mutableStateOf(0) // 0 = player, 1 = opponent - private var _isAttackVisible by mutableStateOf(false) - private var _critBarProgress by mutableStateOf(0) - private var _isAttackButtonEnabled by mutableStateOf(true) - - // Attack animation state - private var _attackIsHit by mutableStateOf(false) - private var _isPlayerAttacking by mutableStateOf(false) - - // Exposed state for Compose - val playerCurrentHP: Float get() = _playerCurrentHP - val playerMaxHP: Float get() = _playerMaxHP - val opponentCurrentHP: Float get() = _opponentCurrentHP - val opponentMaxHP: Float get() = _opponentMaxHP - val isAttacking: Boolean get() = _isAttacking - val attackProgress: Float get() = _attackProgress - val currentView: Int get() = _currentView - val isAttackVisible: Boolean get() = _isAttackVisible - val critBarProgress: Int get() = _critBarProgress - val isAttackButtonEnabled: Boolean get() = _isAttackButtonEnabled - - // Attack animation state - val attackIsHit: Boolean get() = _attackIsHit - val isPlayerAttacking: Boolean get() = _isPlayerAttacking - - //Initialize battle with character data - fun initializeBattle( - playerHP: Float = 100f, - opponentHP: Float = 100f, - playerMaxHP: Float = 100f, - opponentMaxHP: Float = 100f - ) { - _playerCurrentHP = playerHP - _playerMaxHP = playerMaxHP - _opponentCurrentHP = opponentHP - _opponentMaxHP = opponentMaxHP - _currentView = 0 - _isAttacking = false - _attackProgress = 0f - _isAttackVisible = false - _critBarProgress = 0 - _isAttackButtonEnabled = true - - Log.d(TAG, "Battle initialized: Player HP $playerHP/$playerMaxHP, Opponent HP $opponentHP/$opponentMaxHP") - } - - //Start player attack - fun startPlayerAttack() { - _isAttacking = true - _currentView = 0 - _isAttackVisible = true - _isAttackButtonEnabled = false - _isPlayerAttacking = true - _attackProgress = 0f - Log.d(TAG, "Player attack started") - } - - //Start opponent attack - fun startOpponentAttack() { - _isAttacking = true - _currentView = 1 - _isAttackVisible = true - _isPlayerAttacking = false - _attackProgress = 0f - Log.d(TAG, "Opponent attack started") - } - - //Update attack animation progress - fun updateAttackAnimation(progress: Float) { - _attackProgress = progress - } - - //Complete attack animation - fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { - _isAttacking = false - _isAttackVisible = false - _attackProgress = 0f - _attackIsHit = false - - // Apply damage at end of animation - if (playerDamage > 0) { - applyDamage(true, playerDamage) // Player takes damage - println("Player took $playerDamage damage at end of animation") - } - if (opponentDamage > 0) { - applyDamage(false, opponentDamage) // Opponent takes damage - println("Opponent took $opponentDamage damage at end of animation") - } - - // Don't enable attack button here - it will be enabled when we switch back to player view - Log.d(TAG, "Attack animation completed") - } - - //Apply damage to player or opponent - fun applyDamage(isPlayer: Boolean, damage: Float) { - if (isPlayer) { - _playerCurrentHP = (_playerCurrentHP - damage).coerceAtLeast(0f) - Log.d(TAG, "Player took $damage damage. HP: ${_playerCurrentHP}/${_playerMaxHP}") - } else { - _opponentCurrentHP = (_opponentCurrentHP - damage).coerceAtLeast(0f) - Log.d(TAG, "Opponent took $damage damage. HP: ${_opponentCurrentHP}/${_opponentMaxHP}") - } - } - - //Update critical bar progress - fun updateCritBarProgress(progress: Int) { - _critBarProgress = progress - } - - // Update HP from API response - fun updateHPFromAPI(playerHP: Float, opponentHP: Float) { - _playerCurrentHP = playerHP.coerceAtLeast(0f) - _opponentCurrentHP = opponentHP.coerceAtLeast(0f) - Log.d(TAG, "HP updated from API: Player HP $playerHP, Opponent HP $opponentHP") - } - - - //Check if battle is over - fun isBattleOver(): Boolean { - return _playerCurrentHP <= 0f || _opponentCurrentHP <= 0f - } - - //Get battle winner - fun getWinner(): String? { - return when { - _playerCurrentHP <= 0f -> "opponent" - _opponentCurrentHP <= 0f -> "player" - else -> null - } - } - - //Reset battle state - fun resetBattle() { - _playerCurrentHP = _playerMaxHP - _opponentCurrentHP = _opponentMaxHP - _isAttacking = false - _attackProgress = 0f - _currentView = 0 - _isAttackVisible = false - _critBarProgress = 0 - _isAttackButtonEnabled = true - Log.d(TAG, "Battle reset") - } - - //Clean up resources - fun cleanup() { - _isAttacking = false - _isAttackVisible = false - _attackProgress = 0f - Log.d(TAG, "Battle system cleaned up") - } - - // Enable attack button - fun enableAttackButton() { - _isAttackButtonEnabled = true - Log.d(TAG, "Attack button enabled") - } - - // Disable attack button - fun disableAttackButton() { - _isAttackButtonEnabled = false - Log.d(TAG, "Attack button disabled") - } - - // Set attack hit/miss state - fun setAttackHitState(isHit: Boolean) { - _attackIsHit = isHit - Log.d(TAG, "Attack hit state set to: $isHit") - } - - // Switch to specific view - fun switchToView(view: Int) { - _currentView = view - Log.d(TAG, "Switched to view: $view") - } -} +import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem @Composable fun BattleScreen( - battleSystem: ArenaBattleSystem, - stage: String = "rookie", - playerName: String = "Player", - opponentName: String = "Opponent", - activeCharacter: APIBattleCharacter? = null, - opponentCharacter: APIBattleCharacter? = null, - onBattleComplete: (String?) -> Unit = {}, - onExitBattle: () -> Unit = {}, + stage: String, + playerName: String, + opponentName: String, + activeCharacter: APIBattleCharacter?, + opponentCharacter: APIBattleCharacter?, + onAttackClick: () -> Unit, context: android.content.Context? = null ) { - var animationProgress by remember { mutableStateOf(0f) } + val battleSystem = remember { ArenaBattleSystem() } + + // Pending damage state for API integration var pendingPlayerDamage by remember { mutableStateOf(0f) } var pendingOpponentDamage by remember { mutableStateOf(0f) } - var currentAttackType by remember { mutableStateOf("") } // Track if this is player or enemy attack - + // Critical bar timer LaunchedEffect(Unit) { while (true) { delay(30) - if (!battleSystem.isAttacking) { + if (battleSystem.attackPhase == 0) { // Only update when not attacking battleSystem.updateCritBarProgress((battleSystem.critBarProgress + 5) % 101) } } } - - // Player attack animation - LaunchedEffect(battleSystem.isAttacking, battleSystem.isPlayerAttacking) { - if (battleSystem.isAttacking && battleSystem.isPlayerAttacking) { - animationProgress = 0f - println("=== Starting PLAYER attack animation ===") - println("Current view: ${battleSystem.currentView}") - println("Is player attacking: ${battleSystem.isPlayerAttacking}") - - // Store the current attack type - currentAttackType = "player" - println("Attack type: $currentAttackType") - - // Complete attack animation across the full screen - animate( - initialValue = 0f, - targetValue = 1f, - animationSpec = tween(ArenaBattleSystem.ANIMATION_DURATION.toInt()) - ) { value, _ -> - animationProgress = value - battleSystem.updateAttackAnimation(value) - println("Animation progress: $value") - } - - println("=== PLAYER Animation completed ===") - println("Final animation progress: $animationProgress") - - // Complete the attack animation - battleSystem.completeAttackAnimation(pendingPlayerDamage, pendingOpponentDamage) - - // Reset pending damage - pendingPlayerDamage = 0f - pendingOpponentDamage = 0f - - // Player attack just completed - switch to enemy view - println("Player attack completed, switching to enemy view") - battleSystem.switchToView(1) - - // Check if battle is over - if (battleSystem.isBattleOver()) { - onBattleComplete(battleSystem.getWinner()) - } - } - } - // Enemy attack trigger (separate from animation) - LaunchedEffect(currentAttackType) { - if (currentAttackType == "player") { - // Player attack just completed - trigger enemy attack after delay - delay(500) // Wait before enemy attacks back - println("Starting enemy attack") - battleSystem.startOpponentAttack() - println("Enemy attack triggered - LaunchedEffect should re-trigger") - } - } - - // Enemy attack animation - LaunchedEffect(battleSystem.isAttacking, battleSystem.isPlayerAttacking) { - if (battleSystem.isAttacking && !battleSystem.isPlayerAttacking) { - animationProgress = 0f - println("=== Starting ENEMY attack animation ===") - println("Current view: ${battleSystem.currentView}") - println("Is player attacking: ${battleSystem.isPlayerAttacking}") - - // Store the current attack type - currentAttackType = "enemy" - println("Attack type: $currentAttackType") - - // Complete attack animation across the full screen - animate( - initialValue = 0f, - targetValue = 1f, - animationSpec = tween(ArenaBattleSystem.ANIMATION_DURATION.toInt()) - ) { value, _ -> - animationProgress = value - battleSystem.updateAttackAnimation(value) - println("Animation progress: $value") + // Animation for attack phases + LaunchedEffect(battleSystem.attackPhase) { + when (battleSystem.attackPhase) { + 1 -> { + // Phase 1: Player attack on player screen + println("Starting Phase 1: Player attack on player screen") + var progress = 0f + while (progress < 1f) { + progress += 0.016f // 60 FPS + battleSystem.setAttackProgress(progress) + delay(16) // 60 FPS + } + println("Phase 1 completed, advancing to Phase 2") + battleSystem.advanceAttackPhase() } - - println("=== ENEMY Animation completed ===") - println("Final animation progress: $animationProgress") - - // Complete the attack animation - battleSystem.completeAttackAnimation(pendingPlayerDamage, pendingOpponentDamage) - - // Reset pending damage - pendingPlayerDamage = 0f - pendingOpponentDamage = 0f - - // Enemy attack just completed - switch back to player view - println("Enemy attack completed, switching back to player view") - battleSystem.switchToView(0) - // Enable attack button when we're back on player view - battleSystem.enableAttackButton() - - // Check if battle is over - if (battleSystem.isBattleOver()) { - onBattleComplete(battleSystem.getWinner()) + 2 -> { + // Phase 2: Player attack on opponent screen + println("Starting Phase 2: Player attack on opponent screen") + battleSystem.switchToView(1) + var progress = 0f + while (progress < 1f) { + progress += 0.016f // 60 FPS + battleSystem.setAttackProgress(progress) + delay(16) // 60 FPS + } + println("Phase 2 completed, applying damage and starting opponent attack") + // Apply player's damage and start opponent attack + battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) + pendingOpponentDamage = 0f + delay(500) + battleSystem.startOpponentAttack() + } + 3 -> { + // Phase 3: Opponent attack on opponent screen + println("Starting Phase 3: Opponent attack on opponent screen") + battleSystem.switchToView(1) + var progress = 0f + while (progress < 1f) { + progress += 0.016f // 60 FPS + battleSystem.setAttackProgress(progress) + delay(16) // 60 FPS + } + println("Phase 3 completed, advancing to Phase 4") + battleSystem.advanceAttackPhase() + } + 4 -> { + // Phase 4: Opponent attack on player screen + println("Starting Phase 4: Opponent attack on player screen") + battleSystem.switchToView(0) + var progress = 0f + while (progress < 1f) { + progress += 0.016f // 60 FPS + battleSystem.setAttackProgress(progress) + delay(16) // 60 FPS + } + println("Phase 4 completed, applying damage and resetting") + // Apply opponent's damage and reset + battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + pendingPlayerDamage = 0f + battleSystem.resetAttackState() + battleSystem.enableAttackButton() + + // Check if battle is over + if (battleSystem.checkBattleOver()) { + battleSystem.endBattle() + onAttackClick() + } } } } Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black) + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - //println("Current battle view: ${battleSystem.currentView}") when (battleSystem.currentView) { 0 -> { - //println("Showing PlayerBattleView") + // Player view PlayerBattleView( battleSystem = battleSystem, stage = stage, playerName = playerName, - attackAnimationProgress = animationProgress, + attackAnimationProgress = battleSystem.attackProgress, onAttackClick = { battleSystem.startPlayerAttack() - // Damage will be applied at end of animation via pending damage system }, activeCharacter = activeCharacter, context = context, @@ -399,27 +172,16 @@ fun BattleScreen( ) } 1 -> { - //println("Showing OpponentBattleView") - OpponentBattleView( - battleSystem = battleSystem, - stage = stage, - opponentName = opponentName, - attackAnimationProgress = animationProgress, - activeCharacter = opponentCharacter - ) + // Opponent view + OpponentBattleView( + battleSystem = battleSystem, + stage = stage, + opponentName = opponentName, + attackAnimationProgress = battleSystem.attackProgress, + activeCharacter = opponentCharacter + ) } } - - // Exit button - Button( - onClick = onExitBattle, - modifier = Modifier - .align(Alignment.TopCenter) - .padding(16.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color.Red) - ) { - Text("Exit", color = Color.White) - } } } @@ -430,54 +192,18 @@ fun PlayerBattleView( playerName: String, attackAnimationProgress: Float, onAttackClick: () -> Unit, - activeCharacter: APIBattleCharacter? = null, - context: android.content.Context? = null, - opponent: APIBattleCharacter? = null, + activeCharacter: APIBattleCharacter?, + context: android.content.Context?, + opponent: APIBattleCharacter?, onSetPendingDamage: (Float, Float) -> Unit ) { Column( modifier = Modifier .fillMaxSize() .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + horizontalAlignment = Alignment.CenterHorizontally ) { - // Health display - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - Text("Current HP", color = Color.White, fontSize = 12.sp) - Text( - text = battleSystem.playerCurrentHP.toInt().toString(), - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - Column { - Text("Max HP", color = Color.White, fontSize = 12.sp) - Text( - text = battleSystem.playerMaxHP.toInt().toString(), - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - } - - // Health bar - LinearProgressIndicator( - progress = (battleSystem.playerCurrentHP / battleSystem.playerMaxHP).coerceIn(0f, 1f), - modifier = Modifier - .fillMaxWidth() - .height(20.dp), - color = Color.Green, - trackColor = Color.Gray - ) - - // Player character with attack animation + // Player Digimon Box( modifier = Modifier .fillMaxWidth() @@ -485,50 +211,44 @@ fun PlayerBattleView( contentAlignment = Alignment.CenterStart ) { SpriteImage( - spriteName = "00", // The sprite number (00, 01, 02, etc.) - atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim000_mon03" - "champion" -> "dim012_mon04" - "ultimate" -> "dim137_mon09" - "mega" -> "dim012_mon14" - else -> "dim011_mon01" - }, + spriteName = activeCharacter?.charaId ?: "dim011_mon01", + atlasName = "dim011_mon01", modifier = Modifier .size(80.dp) - .scale(-1f, 1f), // Flip horizontally + .scale(-1f, 1f), // Flip player Digimon horizontally contentScale = ContentScale.Fit ) - - // Attack animation overlay - if (attackAnimationProgress > 0) { - // Show player attack on both player and enemy screens - // Show enemy attack on both enemy and player screens - val shouldShowAttack = true // Always show attack during animation - - if (shouldShowAttack) { - val xOffset = if (battleSystem.isPlayerAttacking) { - // Player attack: start from player position (left side) and fly to right edge - (attackAnimationProgress * 400 - 200).dp - } else { - // Enemy attack: start from right edge and fly to left edge - (-attackAnimationProgress * 400 + 200).dp - } - - println("Attack sprite - Progress: $attackAnimationProgress, IsPlayerAttacking: ${battleSystem.isPlayerAttacking}, X Offset: $xOffset") - - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks - contentScale = ContentScale.Fit - ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { + 1 -> true // Player attack on player screen + 2 -> true // Player attack on opponent screen + 3 -> false // Opponent attack on opponent screen + 4 -> false // Opponent attack on player screen + else -> false + } + + if (shouldShowAttack) { + val xOffset = when (battleSystem.attackPhase) { + 1 -> (attackAnimationProgress * 400 - 200).dp // Player attack on player screen + 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen + else -> 0.dp } + + println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(-1f, 1f), // Flip player attacks + contentScale = ContentScale.Fit + ) } } @@ -543,7 +263,6 @@ fun PlayerBattleView( ) // Attack button - //println("PlayerBattleView: Attack button enabled = ${battleSystem.isAttackButtonEnabled}") Button( onClick = { println("Attack button clicked!") @@ -595,7 +314,6 @@ fun PlayerBattleView( // Handle damage timing based on hit/miss if (apiResult.playerAttackHit) { // Player attack hit - enemy takes damage at end of player animation - // Player will dodge enemy attack println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage battleSystem.setAttackHitState(true) @@ -606,42 +324,24 @@ fun PlayerBattleView( battleSystem.setAttackHitState(false) } - // Don't update HP immediately - wait for animation to complete - // battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) - - // Keep attack button enabled for next round + // Update HP from API + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + } + 2 -> { + // Match is over - transition to results screen + println("Match is over! Winner: ${apiResult.winner}") + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + onAttackClick() // This will transition to battle-results screen + } + -1 -> { + // Error occurred + println("API Error: ${apiResult.status}") + battleSystem.resetAttackState() battleSystem.enableAttackButton() } - 2 -> { - // Match is over - report winner and complete battle - println("Match over! Winner: ${apiResult.winner}") - println("Final HP - Player: ${apiResult.playerHP}, Opponent: ${apiResult.opponentHP}") - - // Don't update HP immediately - let animation complete first - // battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) - - // Disable attack button since match is over - battleSystem.disableAttackButton() - - // Complete the battle - onAttackClick() - } - -1 -> { - // Error state - println("API Error: ${apiResult.status}") - // Re-enable attack button on error - battleSystem.enableAttackButton() - } - else -> { - println("Unknown state: ${apiResult.state}") - // Re-enable attack button on unknown state - battleSystem.enableAttackButton() - } - } - } + } + } } - - // Don't call onAttackClick() here - only call it when match is over (state = 2) }, enabled = battleSystem.isAttackButtonEnabled, modifier = Modifier @@ -671,44 +371,9 @@ fun OpponentBattleView( .fillMaxSize() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.Center ) { - // Health display - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - Text("Current HP", color = Color.White, fontSize = 12.sp) - Text( - text = battleSystem.opponentCurrentHP.toInt().toString(), - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - Column { - Text("Max HP", color = Color.White, fontSize = 12.sp) - Text( - text = battleSystem.opponentMaxHP.toInt().toString(), - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - } - - // Health bar - LinearProgressIndicator( - progress = (battleSystem.opponentCurrentHP / battleSystem.opponentMaxHP).coerceIn(0f, 1f), - modifier = Modifier - .fillMaxWidth() - .height(20.dp), - color = Color.Green, - trackColor = Color.Gray - ) - - // Opponent character with attack animation + // Opponent Digimon Box( modifier = Modifier .fillMaxWidth() @@ -716,49 +381,42 @@ fun OpponentBattleView( contentAlignment = Alignment.CenterEnd ) { SpriteImage( - spriteName = "00", // The sprite number (00, 01, 02, etc.) - atlasName = activeCharacter?.charaId ?: when (stage) { - "rookie" -> "dim000_mon03" - "champion" -> "dim012_mon04" - "ultimate" -> "dim137_mon09" - "mega" -> "dim012_mon14" - else -> "dim011_mon01" - }, - modifier = Modifier - .size(80.dp), + spriteName = activeCharacter?.charaId ?: "dim011_mon01", + atlasName = "dim011_mon01", + modifier = Modifier.size(80.dp), contentScale = ContentScale.Fit ) - - // Attack animation overlay - if (attackAnimationProgress > 0) { - // Show player attack on both player and enemy screens - // Show enemy attack on both enemy and player screens - val shouldShowAttack = true // Always show attack during animation - - if (shouldShowAttack) { - val xOffset = if (battleSystem.isPlayerAttacking) { - // Player attack: start from left edge and fly to right edge - (attackAnimationProgress * 400 - 200).dp - } else { - // Enemy attack: start from enemy position (right side) and fly to left edge - (-attackAnimationProgress * 400 + 200).dp - } - - println("OpponentBattleView - Attack sprite - Progress: $attackAnimationProgress, IsPlayerAttacking: ${battleSystem.isPlayerAttacking}, X Offset: $xOffset") - - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks - contentScale = ContentScale.Fit - ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { + 1 -> false // Player attack on player screen + 2 -> true // Player attack on opponent screen + 3 -> true // Opponent attack on opponent screen + 4 -> false // Opponent attack on player screen + else -> false + } + + if (shouldShowAttack) { + val xOffset = when (battleSystem.attackPhase) { + 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen + 3 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on opponent screen + else -> 0.dp } + + println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks + contentScale = ContentScale.Fit + ) } } @@ -781,6 +439,7 @@ fun BattlesScreen() { var expanded by remember { mutableStateOf(false) } var selectedStage by remember { mutableStateOf("") } + var currentStage by remember { mutableStateOf("rookie") } val context = LocalContext.current @@ -798,35 +457,18 @@ fun BattlesScreen() { val rookieButton = @Composable { Button( onClick = { - //println("Rookie button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "rookie") { opponents -> - //println("API call completed successfully") try { - //println("Received opponents data: $opponents") - //println("Opponents list size: ${opponents.opponentsList.size}") - - // For loop to check opponents and print their names - //for (opponent in opponents.opponentsList) { - // println("Opponent: ${opponent.name}") - //} - - // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - - //println("Updated opponentsList size: ${opponentsList.size}") - //println("About to change view to rookie") currentView = "rookie" - //println("View changed to rookie") } catch (e: Exception) { - //println("Error processing opponents data: ${e.message}") Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - //println("Error calling getOpponents: ${e.message}") Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } @@ -839,35 +481,18 @@ fun BattlesScreen() { val championButton = @Composable { Button( onClick = { - //println("Champion button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "champion") { opponents -> - //println("API call completed successfully") try { - //println("Received opponents data: $opponents") - //println("Opponents list size: ${opponents.opponentsList.size}") - - // For loop to check opponents and print their names - //for (opponent in opponents.opponentsList) { - // println("Opponent: ${opponent.name}") - //} - - // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - - //println("Updated opponentsList size: ${opponentsList.size}") - //println("About to change view to champion") currentView = "champion" - //println("View changed to champion") } catch (e: Exception) { - //println("Error processing opponents data: ${e.message}") Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - //println("Error calling getOpponents: ${e.message}") Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } @@ -880,35 +505,18 @@ fun BattlesScreen() { val ultimateButton = @Composable { Button( onClick = { - //println("Ultimate button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "ultimate") { opponents -> - //println("API call completed successfully") try { - //println("Received opponents data: $opponents") - //println("Opponents list size: ${opponents.opponentsList.size}") - - // For loop to check opponents and print their names - //for (opponent in opponents.opponentsList) { - // println("Opponent: ${opponent.name}") - //} - - // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - - //println("Updated opponentsList size: ${opponentsList.size}") - //println("About to change view to ultimate") currentView = "ultimate" - //println("View changed to ultimate") } catch (e: Exception) { - //println("Error processing opponents data: ${e.message}") Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - //println("Error calling getOpponents: ${e.message}") Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } @@ -921,35 +529,18 @@ fun BattlesScreen() { val megaButton = @Composable { Button( onClick = { - //println("Mega button clicked - starting API call") try { RetrofitHelper().getOpponents(context, "mega") { opponents -> - //println("API call completed successfully") try { - //println("Received opponents data: $opponents") - //println("Opponents list size: ${opponents.opponentsList.size}") - - // For loop to check opponents and print their names - //for (opponent in opponents.opponentsList) { - // println("Opponent: ${opponent.name}") - //} - - // Store the opponents in your ArrayList opponentsList.clear() opponentsList.addAll(opponents.opponentsList) - - //println("Updated opponentsList size: ${opponentsList.size}") - //println("About to change view to mega") currentView = "mega" - //println("View changed to mega") } catch (e: Exception) { - //println("Error processing opponents data: ${e.message}") Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() } } } catch (e: Exception) { - //println("Error calling getOpponents: ${e.message}") Log.d(TAG,"Error calling getOpponents: ${e.message}") e.printStackTrace() } @@ -961,49 +552,13 @@ fun BattlesScreen() { val backButton = @Composable { Button( - onClick = { - currentView = "main" - } + onClick = { currentView = "main" } ) { Text("Back") } } val characterDropdown = @Composable { currentStage: String -> - // Create hardcoded character lists for each stage - val rookieCharacters = listOf( - APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), - APIBattleCharacter("PULSEMON", "degimon_name_Dim000_003", "dim000_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), - APIBattleCharacter("DORUMON", "degimon_name_dim137_mon03", "dim137_mon03", 0, 1, 3000, 3000, 5100.0f, 1050.0f) - ) - - val championCharacters = listOf( - APIBattleCharacter("GREYMON","degimon_name_Dim012_004","dim012_mon04",1,1,2000, 2000, 3000.0f,900.0f), - APIBattleCharacter("TYRANNOMON","degimon_name_Dim008_006","dim008_mon06",1,3,2000, 2000, 2400.0f,600.0f), - APIBattleCharacter("DORUGAMON","degimon_name_dim137_mon05","dim137_mon05",1,3,3500, 3500, 5200.0f,1200.0f) - ) - - val ultimateCharacters = listOf( - APIBattleCharacter("METALGREYMON (VIRUS)","degimon_name_Dim014_005","dim014_mon05",2,2,2640, 2640, 2450.0f,800.0f), - APIBattleCharacter("MAMEMON", "degimon_name_Dim000_005", "dim000_mon05", 2, 1, 3000, 3000, 4000.0f, 1000.0f), - APIBattleCharacter("DORUGREYMON","degimon_name_dim137_mon09","dim137_mon09",2,3,5000, 5000, 6400.0f,1400.0f) - ) - - val megaCharacters = listOf( - APIBattleCharacter("WARGREYMON","degimon_name_Dim012_014","dim012_mon14",3,1,3080, 3080, 3825.0f,800.0f), - APIBattleCharacter("SLAYERDRAMON","degimon_name_dim129_mon15","dim129_mon15",3,1,4800, 4800, 6300.0f,1950.0f), - APIBattleCharacter("BREAKDRAMON","degimon_name_dim129_mon17","dim129_mon17",3,2,6000, 6000, 4000.0f,1980.0f) - ) - - // Get the appropriate character list based on current stage - val characterList = when (currentStage.lowercase()) { - "rookie" -> rookieCharacters - "champion" -> championCharacters - "ultimate" -> ultimateCharacters - "mega" -> megaCharacters - else -> rookieCharacters - } - ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded } @@ -1070,7 +625,6 @@ fun BattlesScreen() { } } - "rookie" -> { Column( horizontalAlignment = Alignment.CenterHorizontally @@ -1110,159 +664,129 @@ fun BattlesScreen() { } "champion" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Champion Battle View") + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Champion Battle View") - // Add character selection dropdown - characterDropdown("champion") + // Add character selection dropdown + characterDropdown("champion") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> - currentView = "battle-main" + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> + currentView = "battle-main" + } } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } } - } - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}") - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") - } + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } - backButton() - } + backButton() + } } "ultimate" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Ultimate Battle View") + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Ultimate Battle View") - // Add character selection dropdown - characterDropdown("ultimate") + // Add character selection dropdown + characterDropdown("ultimate") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> - currentView = "battle-main" + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> + currentView = "battle-main" + } } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } } - } - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}") - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") - } + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } - backButton() - } + backButton() + } } "mega" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Mega Battle View") + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Mega Battle View") - // Add character selection dropdown - characterDropdown("mega") + // Add character selection dropdown + characterDropdown("mega") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> - currentView = "battle-main" + // Display buttons for each opponent + opponentsList.forEach { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> + currentView = "battle-main" + } } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("Battle ${opponent.name}") + } } - } - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}") - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") - } + // Show selected character info + activeCharacter?.let { character -> + Text("Active Character: ${character.name}") + Text("HP: ${character.currentHp}/${character.baseHp}") + Text("BP: ${character.baseBp}") + Text("AP: ${character.baseAp}") + } - backButton() - } + backButton() + } } "battle-main" -> { - val battleSystem = remember { ArenaBattleSystem() } - - // Determine the current stage based on the character's stage - val currentStage = when (activeCharacter?.stage) { - 0 -> "rookie" - 1 -> "champion" - 2 -> "ultimate" - 3 -> "mega" - else -> "rookie" - } - - // Initialize battle with character stats - LaunchedEffect(activeCharacter, selectedOpponent) { - activeCharacter?.let { playerCharacter -> - selectedOpponent?.let { opponentCharacter -> - println("Initializing battle with player: ${playerCharacter.name}, opponent: ${opponentCharacter.name}") - battleSystem.initializeBattle( - playerHP = playerCharacter.currentHp.toFloat(), - opponentHP = opponentCharacter.currentHp.toFloat(), - playerMaxHP = playerCharacter.baseHp.toFloat(), - opponentMaxHP = opponentCharacter.baseHp.toFloat() - ) - } - } - } - BattleScreen( - battleSystem = battleSystem, stage = currentStage, playerName = activeCharacter?.name ?: "Player", opponentName = selectedOpponent?.name ?: "Opponent", activeCharacter = activeCharacter, opponentCharacter = selectedOpponent, - onBattleComplete = { winner -> - println("Battle complete! Winner: $winner") - currentView = "battle-results" - }, - onExitBattle = { - currentView = "main" + onAttackClick = { + // This will be called when the battle is over + currentView = "battle-results" }, context = context ) @@ -1272,7 +796,6 @@ fun BattlesScreen() { var winnerName by remember { mutableStateOf("") } var isWinnerLoaded by remember { mutableStateOf(false) } - // Send one more stage 1 call to get winner info, then cleanup LaunchedEffect(Unit) { // Determine player and opponent stages val playerStage = when (activeCharacter?.stage) { @@ -1291,7 +814,7 @@ fun BattlesScreen() { else -> 0 } - // First, send one more stage 1 call to get winner info + // First get the winner info RetrofitHelper().getPVPWinner( context, 1, @@ -1301,12 +824,11 @@ fun BattlesScreen() { opponentStage, selectedOpponent?.name ?: "Opponent", opponentStage - ) { winnerResult -> - println("Winner API Result: $winnerResult") - winnerName = winnerResult.winner + ) { apiResult -> + winnerName = apiResult.winner ?: "Unknown" isWinnerLoaded = true - // Now send cleanup call + // Then send the cleanup call RetrofitHelper().getPVPWinner( context, 2, @@ -1317,54 +839,50 @@ fun BattlesScreen() { selectedOpponent?.name ?: "Opponent", opponentStage ) { cleanupResult -> - println("Cleanup API Result: $cleanupResult") + println("Cleanup call completed") } } } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - Text( - text = "Battle Complete!", - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = Color.Gray - ) - - Spacer(modifier = Modifier.height(16.dp)) - - if (isWinnerLoaded) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { Text( - text = "Winner: $winnerName", - fontSize = 18.sp, + text = "Battle Complete!", + fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.Gray ) - } else { - Text( - text = "Loading results...", - fontSize = 16.sp, - color = Color.Gray - ) + + Spacer(modifier = Modifier.height(16.dp)) + + if (isWinnerLoaded) { + Text( + text = "Winner: $winnerName", + fontSize = 20.sp, + color = Color.Gray + ) + } else { + Text( + text = "Loading results...", + fontSize = 20.sp, + color = Color.Gray + ) + } } - Spacer(modifier = Modifier.height(32.dp)) - + // Exit button Button( - onClick = { - currentView = "main" - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Blue - ), - shape = RoundedCornerShape(8.dp) + onClick = { currentView = "main" }, + modifier = Modifier.align(Alignment.TopCenter), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) ) { - Text("Back to Main Menu", color = Color.White, fontSize = 16.sp) + Text("Exit", color = Color.White) } } } @@ -1372,3 +890,12 @@ fun BattlesScreen() { } } } + +// Character list for dropdown +val characterList = listOf( + APIBattleCharacter("Agumon", "agumon", "dim011_mon01", 0, 0, 100, 100, 50f, 50f), + APIBattleCharacter("Gabumon", "gabumon", "dim012_mon02", 0, 0, 90, 90, 45f, 55f), + APIBattleCharacter("Biyomon", "biyomon", "dim013_mon03", 0, 0, 85, 85, 40f, 60f), + APIBattleCharacter("Tentomon", "tentomon", "dim014_mon04", 0, 0, 95, 95, 55f, 45f), + APIBattleCharacter("Palmon", "palmon", "dim015_mon05", 0, 0, 88, 88, 42f, 58f) +) \ No newline at end of file From b3a4ced28d039ef8998297fa015d7d8de9260e08 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 10:48:06 -0400 Subject: [PATCH 20/89] Commented out crit bar progress to stop log noise. --- .../com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2c3fc37..fa8d84e 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 @@ -129,6 +129,6 @@ class ArenaBattleSystem { fun updateCritBarProgress(progress: Int) { _critBarProgress = progress - Log.d(TAG, "Updated crit bar progress: $progress") + //Log.d(TAG, "Updated crit bar progress: $progress") } } \ No newline at end of file From 4e12962c055a833e03d2e0cdf693ef70d1673f02 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 10:48:21 -0400 Subject: [PATCH 21/89] Fixed sprite naming bug. --- .../vbhelper/battle/BattleSpriteManager.kt | 2 +- .../vbhelper/screens/BattlesScreen.kt | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 05d0e34..5347c5e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -73,7 +73,7 @@ class BattleSpriteManager(private val context: Context) { } // Load the specific sprite data file - val spriteDataFile = File(spriteBaseDir, "sprites/${atlasName}_sprite_${spriteName}.json") + val spriteDataFile = File(spriteBaseDir, "sprites/${spriteName}_sprite_00.json") if (!spriteDataFile.exists()) { println("Sprite data file not found: ${spriteDataFile.absolutePath}") return null 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 b354253..8fc2d47 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 @@ -212,7 +212,7 @@ fun PlayerBattleView( ) { SpriteImage( spriteName = activeCharacter?.charaId ?: "dim011_mon01", - atlasName = "dim011_mon01", + atlasName = activeCharacter?.charaId ?: "dim011_mon01", modifier = Modifier .size(80.dp) .scale(-1f, 1f), // Flip player Digimon horizontally @@ -382,7 +382,7 @@ fun OpponentBattleView( ) { SpriteImage( spriteName = activeCharacter?.charaId ?: "dim011_mon01", - atlasName = "dim011_mon01", + atlasName = activeCharacter?.charaId ?: "dim011_mon01", modifier = Modifier.size(80.dp), contentScale = ContentScale.Fit ) @@ -441,6 +441,40 @@ fun BattlesScreen() { var selectedStage by remember { mutableStateOf("") } var currentStage by remember { mutableStateOf("rookie") } + // Create hardcoded character lists for each stage + val rookieCharacters = listOf( + APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("PULSEMON", "degimon_name_Dim000_003", "dim000_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), + APIBattleCharacter("DORUMON", "degimon_name_dim137_mon03", "dim137_mon03", 0, 1, 3000, 3000, 5100.0f, 1050.0f) + ) + + val championCharacters = listOf( + APIBattleCharacter("GREYMON","degimon_name_Dim012_004","dim012_mon04",1,1,2000, 2000, 3000.0f,900.0f), + APIBattleCharacter("TYRANNOMON","degimon_name_Dim008_006","dim008_mon06",1,3,2000, 2000, 2400.0f,600.0f), + APIBattleCharacter("DORUGAMON","degimon_name_dim137_mon05","dim137_mon05",1,3,3500, 3500, 5200.0f,1200.0f) + ) + + val ultimateCharacters = listOf( + APIBattleCharacter("METALGREYMON (VIRUS)","degimon_name_Dim014_005","dim014_mon05",2,2,2640, 2640, 2450.0f,800.0f), + APIBattleCharacter("MAMEMON", "degimon_name_Dim000_005", "dim000_mon05", 2, 1, 3000, 3000, 4000.0f, 1000.0f), + APIBattleCharacter("DORUGREYMON","degimon_name_dim137_mon09","dim137_mon09",2,3,5000, 5000, 6400.0f,1400.0f) + ) + + val megaCharacters = listOf( + APIBattleCharacter("WARGREYMON","degimon_name_Dim012_014","dim012_mon14",3,1,3080, 3080, 3825.0f,800.0f), + APIBattleCharacter("SLAYERDRAMON","degimon_name_dim129_mon15","dim129_mon15",3,1,4800, 4800, 6300.0f,1950.0f), + APIBattleCharacter("BREAKDRAMON","degimon_name_dim129_mon17","dim129_mon17",3,2,6000, 6000, 4000.0f,1980.0f) + ) + + // Get the appropriate character list based on current stage + val characterList = when (currentStage.lowercase()) { + "rookie" -> rookieCharacters + "champion" -> championCharacters + "ultimate" -> ultimateCharacters + "mega" -> megaCharacters + else -> rookieCharacters + } + val context = LocalContext.current // Initialize sprite files on first load @@ -889,13 +923,4 @@ fun BattlesScreen() { } } } -} - -// Character list for dropdown -val characterList = listOf( - APIBattleCharacter("Agumon", "agumon", "dim011_mon01", 0, 0, 100, 100, 50f, 50f), - APIBattleCharacter("Gabumon", "gabumon", "dim012_mon02", 0, 0, 90, 90, 45f, 55f), - APIBattleCharacter("Biyomon", "biyomon", "dim013_mon03", 0, 0, 85, 85, 40f, 60f), - APIBattleCharacter("Tentomon", "tentomon", "dim014_mon04", 0, 0, 95, 95, 55f, 45f), - APIBattleCharacter("Palmon", "palmon", "dim015_mon05", 0, 0, 88, 88, 42f, 58f) -) \ No newline at end of file +} \ No newline at end of file From 7af8e00e6fc891c5a0f5bc2f73c03d7f8f1328a5 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 11:02:51 -0400 Subject: [PATCH 22/89] Re-added missing exit button and health bars. --- .../vbhelper/battle/ArenaBattleSystem.kt | 6 ++ .../vbhelper/screens/BattlesScreen.kt | 95 +++++++++++++++++-- 2 files changed, 95 insertions(+), 6 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 fa8d84e..09999f7 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 @@ -99,6 +99,12 @@ class ArenaBattleSystem { Log.d(TAG, "Updated HP from API: Player=$playerHP, Opponent=$opponentHP") } + fun initializeHP(playerMaxHP: Float, opponentMaxHP: Float) { + _playerHP = playerMaxHP + _opponentHP = opponentMaxHP + Log.d(TAG, "Initialized HP: Player=$playerMaxHP, Opponent=$opponentMaxHP") + } + fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { if (playerDamage > 0f) { applyDamage(true, playerDamage) 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 8fc2d47..af5ef13 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 @@ -62,6 +62,13 @@ fun BattleScreen( ) { val battleSystem = remember { ArenaBattleSystem() } + // Initialize HP when battle starts + LaunchedEffect(activeCharacter, opponentCharacter) { + val playerMaxHP = activeCharacter?.baseHp?.toFloat() ?: 100f + val opponentMaxHP = opponentCharacter?.baseHp?.toFloat() ?: 100f + battleSystem.initializeHP(playerMaxHP, opponentMaxHP) + } + // Pending damage state for API integration var pendingPlayerDamage by remember { mutableStateOf(0f) } var pendingOpponentDamage by remember { mutableStateOf(0f) } @@ -197,13 +204,29 @@ fun PlayerBattleView( opponent: APIBattleCharacter?, onSetPendingDamage: (Float, Float) -> Unit ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier.fillMaxSize() ) { - // Player Digimon + // Exit button at the top-right + Button( + onClick = { /* TODO: Add exit functionality */ }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + ) { + Text("Exit", color = Color.White, fontSize = 14.sp) + } + + // Main content + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + // Player Digimon Box( modifier = Modifier .fillMaxWidth() @@ -262,6 +285,46 @@ fun PlayerBattleView( trackColor = Color.Gray ) + Spacer(modifier = Modifier.height(16.dp)) + + // Health bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Health display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Enemy HP bar + LinearProgressIndicator( + progress = battleSystem.opponentHP / (opponent?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray + ) + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponent?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(16.dp)) + // Attack button Button( onClick = { @@ -355,6 +418,9 @@ fun PlayerBattleView( ) { Text("Attack", color = Color.White, fontSize = 18.sp) } + + Spacer(modifier = Modifier.height(16.dp)) + } } } @@ -420,6 +486,23 @@ fun OpponentBattleView( } } + // Enemy HP bar + LinearProgressIndicator( + progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray + ) + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + // Spacer for layout balance Spacer(modifier = Modifier.height(120.dp)) } From 31fab9dba45a38a29d96d6d7d0d571d41e1f3148 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 11:13:01 -0400 Subject: [PATCH 23/89] Fixed damage application timing. --- .../vbhelper/screens/BattlesScreen.kt | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) 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 af5ef13..3aa7348 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 @@ -108,12 +108,12 @@ fun BattleScreen( battleSystem.setAttackProgress(progress) delay(16) // 60 FPS } - println("Phase 2 completed, applying damage and starting opponent attack") - // Apply player's damage and start opponent attack - battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) - pendingOpponentDamage = 0f - delay(500) - battleSystem.startOpponentAttack() + println("Phase 2 completed, applying damage and starting opponent attack") + // Apply player's damage and start opponent attack + battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) + pendingOpponentDamage = 0f + delay(500) + battleSystem.startOpponentAttack() } 3 -> { // Phase 3: Opponent attack on opponent screen @@ -138,12 +138,12 @@ fun BattleScreen( battleSystem.setAttackProgress(progress) delay(16) // 60 FPS } - println("Phase 4 completed, applying damage and resetting") - // Apply opponent's damage and reset - battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) - pendingPlayerDamage = 0f - battleSystem.resetAttackState() - battleSystem.enableAttackButton() + println("Phase 4 completed, applying damage and resetting") + // Apply opponent's damage and reset + battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + pendingPlayerDamage = 0f + battleSystem.resetAttackState() + battleSystem.enableAttackButton() // Check if battle is over if (battleSystem.checkBattleOver()) { @@ -306,25 +306,6 @@ fun PlayerBattleView( Spacer(modifier = Modifier.height(16.dp)) - // Enemy HP bar - LinearProgressIndicator( - progress = battleSystem.opponentHP / (opponent?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), - color = Color.Red, - trackColor = Color.Gray - ) - - // Enemy HP display numbers - Text( - text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponent?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(16.dp)) - // Attack button Button( onClick = { @@ -374,21 +355,18 @@ fun PlayerBattleView( // Match is still ongoing - update HP and continue println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Handle damage timing based on hit/miss - if (apiResult.playerAttackHit) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) - } else { - // Player attack missed - player takes damage at end of enemy animation - println("Player attack missed! Player will take damage from enemy attack") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage - battleSystem.setAttackHitState(false) - } - - // Update HP from API - battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + // Set pending damage based on API result + if (apiResult.playerAttackHit) { + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - enemy counter-attacks and player takes damage + println("Player attack missed! Enemy counter-attacks and player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + battleSystem.setAttackHitState(false) + } } 2 -> { // Match is over - transition to results screen From 9d0e68fb8a5a1e98814d2c8eb7b932eb3f251fd2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 11:22:22 -0400 Subject: [PATCH 24/89] Re-added missing UI pieces (HP bar, HP numbers, & exit button). --- .../vbhelper/screens/BattlesScreen.kt | 349 +++++++++--------- 1 file changed, 182 insertions(+), 167 deletions(-) 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 3aa7348..19578f0 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 @@ -207,197 +207,212 @@ fun PlayerBattleView( Box( modifier = Modifier.fillMaxSize() ) { - // Exit button at the top-right - Button( - onClick = { /* TODO: Add exit functionality */ }, + // Top section: Exit button, HP bar, and HP numbers + Column( modifier = Modifier - .align(Alignment.TopEnd) - .padding(16.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + .fillMaxWidth() + .padding(16.dp) ) { - Text("Exit", color = Color.White, fontSize = 14.sp) + // Exit button at the top-right + Box( + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = { /* TODO: Add exit functionality */ }, + modifier = Modifier.align(Alignment.TopEnd), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + ) { + Text("Exit", color = Color.White, fontSize = 14.sp) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Health bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Health display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) } - // Main content - Column( + // Middle section: Player Digimon only + Box( modifier = Modifier .fillMaxSize() .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + contentAlignment = Alignment.Center ) { - // Player Digimon - Box( - modifier = Modifier - .fillMaxWidth() - .size(80.dp), - contentAlignment = Alignment.CenterStart - ) { - SpriteImage( - spriteName = activeCharacter?.charaId ?: "dim011_mon01", - atlasName = activeCharacter?.charaId ?: "dim011_mon01", + // Player Digimon (center) + Box( modifier = Modifier - .size(80.dp) - .scale(-1f, 1f), // Flip player Digimon horizontally - contentScale = ContentScale.Fit - ) - - // Attack sprite visibility and positioning based on attack phase - val shouldShowAttack = when (battleSystem.attackPhase) { - 1 -> true // Player attack on player screen - 2 -> true // Player attack on opponent screen - 3 -> false // Opponent attack on opponent screen - 4 -> false // Opponent attack on player screen - else -> false - } - - if (shouldShowAttack) { - val xOffset = when (battleSystem.attackPhase) { - 1 -> (attackAnimationProgress * 400 - 200).dp // Player attack on player screen - 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen - else -> 0.dp - } - - println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - - AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, + .fillMaxWidth() + .size(80.dp), + contentAlignment = Alignment.Center + ) { + SpriteImage( + spriteName = activeCharacter?.charaId ?: "dim011_mon01", + atlasName = activeCharacter?.charaId ?: "dim011_mon01", modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(-1f, 1f), // Flip player attacks + .size(80.dp) + .scale(-1f, 1f), // Flip player Digimon horizontally contentScale = ContentScale.Fit ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { + 1 -> true // Player attack on player screen + 2 -> true // Player attack on opponent screen + 3 -> false // Opponent attack on opponent screen + 4 -> false // Opponent attack on player screen + else -> false + } + + if (shouldShowAttack) { + val xOffset = when (battleSystem.attackPhase) { + 1 -> (attackAnimationProgress * 400 - 200).dp // Player attack on player screen + 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen + else -> 0.dp + } + + println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(-1f, 1f), // Flip player attacks + contentScale = ContentScale.Fit + ) + } } } - // Critical bar - LinearProgressIndicator( - progress = battleSystem.critBarProgress / 100f, + // Bottom section: Critical bar and Attack button + Column( modifier = Modifier .fillMaxWidth() - .height(10.dp), - color = Color.Yellow, - trackColor = Color.Gray - ) + .padding(16.dp) + .align(Alignment.BottomCenter), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Critical bar + LinearProgressIndicator( + progress = battleSystem.critBarProgress / 100f, + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Yellow, + trackColor = Color.Gray + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - // Health bar - LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), - color = Color.Green, - trackColor = Color.Gray - ) - - // Health display numbers - Text( - text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Attack button - Button( - onClick = { - println("Attack button clicked!") - - // Get crit bar progress as float (0.0f to 100.0f) - val critBarProgressFloat = battleSystem.critBarProgress.toFloat() - - // Determine player and opponent stages - val playerStage = when (activeCharacter?.stage) { - 0 -> 0 // rookie - 1 -> 1 // champion - 2 -> 2 // ultimate - 3 -> 3 // mega - else -> 0 - } - - val opponentStage = when (opponent?.stage) { - 0 -> 0 // rookie - 1 -> 1 // champion - 2 -> 2 // ultimate - 3 -> 3 // mega - else -> 0 - } - - // Send API call with all parameters - context?.let { ctx -> - // Start player attack animation - battleSystem.startPlayerAttack() + // Attack button + Button( + onClick = { + println("Attack button clicked!") - RetrofitHelper().getPVPWinner( - ctx, - 1, - 2, - activeCharacter?.name ?: "Player", - playerStage, - opponentStage, - opponent?.name ?: "Opponent", - opponentStage - ) { apiResult -> - // Handle API response here - println("API Result: $apiResult") + // Get crit bar progress as float (0.0f to 100.0f) + val critBarProgressFloat = battleSystem.critBarProgress.toFloat() + + // Determine player and opponent stages + val playerStage = when (activeCharacter?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + val opponentStage = when (opponent?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + // Send API call with all parameters + context?.let { ctx -> + // Start player attack animation + battleSystem.startPlayerAttack() - // Update HP based on API response - when (apiResult.state) { - 1 -> { - // Match is still ongoing - update HP and continue - println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - - // Set pending damage based on API result - if (apiResult.playerAttackHit) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) - } else { - // Player attack missed - enemy counter-attacks and player takes damage - println("Player attack missed! Enemy counter-attacks and player takes ${apiResult.opponentAttackDamage} damage") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage - battleSystem.setAttackHitState(false) - } - } - 2 -> { - // Match is over - transition to results screen - println("Match is over! Winner: ${apiResult.winner}") - battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) - onAttackClick() // This will transition to battle-results screen - } - -1 -> { - // Error occurred - println("API Error: ${apiResult.status}") - battleSystem.resetAttackState() - battleSystem.enableAttackButton() + RetrofitHelper().getPVPWinner( + ctx, + 1, + 2, + activeCharacter?.name ?: "Player", + playerStage, + opponentStage, + opponent?.name ?: "Opponent", + opponentStage + ) { apiResult -> + // Handle API response here + println("API Result: $apiResult") + + // Update HP based on API response + when (apiResult.state) { + 1 -> { + // Match is still ongoing - update HP and continue + println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") + + // Set pending damage based on API result + if (apiResult.playerAttackHit) { + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - enemy counter-attacks and player takes damage + println("Player attack missed! Enemy counter-attacks and player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + battleSystem.setAttackHitState(false) + } + } + 2 -> { + // Match is over - transition to results screen + println("Match is over! Winner: ${apiResult.winner}") + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + onAttackClick() // This will transition to battle-results screen + } + -1 -> { + // Error occurred + println("API Error: ${apiResult.status}") + battleSystem.resetAttackState() + battleSystem.enableAttackButton() + } } } } - } - }, - enabled = battleSystem.isAttackButtonEnabled, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Red, - disabledContainerColor = Color.Gray - ), - shape = RoundedCornerShape(8.dp) - ) { - Text("Attack", color = Color.White, fontSize = 18.sp) - } - - Spacer(modifier = Modifier.height(16.dp)) + }, + enabled = battleSystem.isAttackButtonEnabled, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Red, + disabledContainerColor = Color.Gray + ), + shape = RoundedCornerShape(8.dp) + ) { + Text("Attack", color = Color.White, fontSize = 18.sp) + } } } } From 8f790eea41d4afcd33c5907a9bf5d63fb911038a Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 11:25:01 -0400 Subject: [PATCH 25/89] Fixed player Digimon positioning. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 19578f0..71b671b 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 @@ -253,12 +253,12 @@ fun PlayerBattleView( .padding(16.dp), contentAlignment = Alignment.Center ) { - // Player Digimon (center) + // Player Digimon (left side) Box( modifier = Modifier .fillMaxWidth() .size(80.dp), - contentAlignment = Alignment.Center + contentAlignment = Alignment.CenterStart ) { SpriteImage( spriteName = activeCharacter?.charaId ?: "dim011_mon01", From eff2fadb5503c7fd8b7f385e0f5ba43a5e14c239 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 11:32:41 -0400 Subject: [PATCH 26/89] Enemy attack sprite now shows on player screen. --- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 71b671b..905379e 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 @@ -274,7 +274,7 @@ fun PlayerBattleView( 1 -> true // Player attack on player screen 2 -> true // Player attack on opponent screen 3 -> false // Opponent attack on opponent screen - 4 -> false // Opponent attack on player screen + 4 -> true // Opponent attack on player screen else -> false } @@ -282,13 +282,20 @@ fun PlayerBattleView( val xOffset = when (battleSystem.attackPhase) { 1 -> (attackAnimationProgress * 400 - 200).dp // Player attack on player screen 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen + 4 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on player screen else -> 0.dp } + // Use opponent character ID for Phase 4 (opponent attack) + val characterId = when (battleSystem.attackPhase) { + 4 -> opponent?.charaId ?: "dim011_mon01" // Use opponent's character ID + else -> activeCharacter?.charaId ?: "dim011_mon01" // Use player's character ID + } + println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", + characterId = characterId, isLarge = true, modifier = Modifier .size(60.dp) @@ -296,7 +303,7 @@ fun PlayerBattleView( x = xOffset, y = 0.dp ) - .scale(-1f, 1f), // Flip player attacks + .scale(if (battleSystem.attackPhase == 4) 1f else -1f, 1f), // Don't flip opponent attacks contentScale = ContentScale.Fit ) } From c8690152bcb1b65d415cb9693e671261043e7b36 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 12:02:41 -0400 Subject: [PATCH 27/89] Fixed player attack sprite on enemy screen. Was showing enemy attack sprite. --- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 905379e..d8f83cb 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 @@ -185,7 +185,8 @@ fun BattleScreen( stage = stage, opponentName = opponentName, attackAnimationProgress = battleSystem.attackProgress, - activeCharacter = opponentCharacter + activeCharacter = opponentCharacter, + playerCharacter = activeCharacter ) } } @@ -430,7 +431,8 @@ fun OpponentBattleView( stage: String, opponentName: String, attackAnimationProgress: Float, - activeCharacter: APIBattleCharacter? = null + activeCharacter: APIBattleCharacter? = null, + playerCharacter: APIBattleCharacter? = null ) { Column( modifier = Modifier @@ -469,10 +471,17 @@ fun OpponentBattleView( else -> 0.dp } + // Use correct character ID based on attack phase + val characterId = when (battleSystem.attackPhase) { + 2 -> playerCharacter?.charaId ?: "dim011_mon01" // Use player's character ID for player attack + 3 -> activeCharacter?.charaId ?: "dim011_mon01" // Use opponent's character ID for opponent attack + else -> "dim011_mon01" + } + println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") AttackSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", + characterId = characterId, isLarge = true, modifier = Modifier .size(60.dp) @@ -480,7 +489,7 @@ fun OpponentBattleView( x = xOffset, y = 0.dp ) - .scale(if (battleSystem.isPlayerAttacking) -1f else 1f, 1f), // Flip player attacks + .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only contentScale = ContentScale.Fit ) } From acd990d32b2f8c20782082860553bb15a3899a24 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 12:06:46 -0400 Subject: [PATCH 28/89] Adjusted player attack sprite positioning. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d8f83cb..84a09a9 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 @@ -281,7 +281,7 @@ fun PlayerBattleView( if (shouldShowAttack) { val xOffset = when (battleSystem.attackPhase) { - 1 -> (attackAnimationProgress * 400 - 200).dp // Player attack on player screen + 1 -> (attackAnimationProgress * 400 + 50).dp // Player attack on player screen - start and end more to the right 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen 4 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on player screen else -> 0.dp @@ -466,7 +466,7 @@ fun OpponentBattleView( if (shouldShowAttack) { val xOffset = when (battleSystem.attackPhase) { - 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen + 2 -> (attackAnimationProgress * 400 - 350).dp // Player attack on opponent screen - start more to the left 3 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on opponent screen else -> 0.dp } From cfa52bce9b04297c4c33be094b46da0ea41fd441 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 12:14:29 -0400 Subject: [PATCH 29/89] Fixed enemy attack sprite positioning. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 84a09a9..eb18dd5 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 @@ -283,7 +283,7 @@ fun PlayerBattleView( val xOffset = when (battleSystem.attackPhase) { 1 -> (attackAnimationProgress * 400 + 50).dp // Player attack on player screen - start and end more to the right 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen - 4 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on player screen + 4 -> (-attackAnimationProgress * 400 + 350).dp // Opponent attack on player screen - start more to the right else -> 0.dp } @@ -467,7 +467,7 @@ fun OpponentBattleView( if (shouldShowAttack) { val xOffset = when (battleSystem.attackPhase) { 2 -> (attackAnimationProgress * 400 - 350).dp // Player attack on opponent screen - start more to the left - 3 -> (-attackAnimationProgress * 400 + 200).dp // Opponent attack on opponent screen + 3 -> (-attackAnimationProgress * 400 + -50).dp // Opponent attack on opponent screen - start more to the left else -> 0.dp } From c5cebd8213d001b3e1b2bd167cf0615a9ee0b8d2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 12:21:41 -0400 Subject: [PATCH 30/89] Fixed bug where enemy attack was showing as player attack sprite for a moment before animation. --- .../vbhelper/screens/BattlesScreen.kt | 88 ++++++++++++++----- 1 file changed, 64 insertions(+), 24 deletions(-) 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 eb18dd5..3b2fdcf 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 @@ -205,6 +205,11 @@ fun PlayerBattleView( opponent: APIBattleCharacter?, onSetPendingDamage: (Float, Float) -> Unit ) { + // Track previous character ID to detect transitions + var previousCharacterId by remember { mutableStateOf(null) } + var previousAttackPhase by remember { mutableStateOf(null) } + var isTransitioning by remember { mutableStateOf(false) } + Box( modifier = Modifier.fillMaxSize() ) { @@ -293,20 +298,35 @@ fun PlayerBattleView( else -> activeCharacter?.charaId ?: "dim011_mon01" // Use player's character ID } + // Handle sprite transition + LaunchedEffect(characterId, battleSystem.attackPhase) { + if ((previousCharacterId != null && previousCharacterId != characterId) || + (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { + // Character ID or attack phase changed, start transition + isTransitioning = true + delay(100) // Brief invisibility period + isTransitioning = false + } + previousCharacterId = characterId + previousAttackPhase = battleSystem.attackPhase + } + println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - AttackSpriteImage( - characterId = characterId, - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(if (battleSystem.attackPhase == 4) 1f else -1f, 1f), // Don't flip opponent attacks - contentScale = ContentScale.Fit - ) + if (!isTransitioning) { + AttackSpriteImage( + characterId = characterId, + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.attackPhase == 4) 1f else -1f, 1f), // Don't flip opponent attacks + contentScale = ContentScale.Fit + ) + } } } } @@ -434,6 +454,11 @@ fun OpponentBattleView( activeCharacter: APIBattleCharacter? = null, playerCharacter: APIBattleCharacter? = null ) { + // Track previous character ID to detect transitions + var previousCharacterId by remember { mutableStateOf(null) } + var previousAttackPhase by remember { mutableStateOf(null) } + var isTransitioning by remember { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxSize() @@ -478,20 +503,35 @@ fun OpponentBattleView( else -> "dim011_mon01" } + // Handle sprite transition + LaunchedEffect(characterId, battleSystem.attackPhase) { + if ((previousCharacterId != null && previousCharacterId != characterId) || + (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { + // Character ID or attack phase changed, start transition + isTransitioning = true + delay(100) // Brief invisibility period + isTransitioning = false + } + previousCharacterId = characterId + previousAttackPhase = battleSystem.attackPhase + } + println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - AttackSpriteImage( - characterId = characterId, - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only - contentScale = ContentScale.Fit - ) + if (!isTransitioning) { + AttackSpriteImage( + characterId = characterId, + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only + contentScale = ContentScale.Fit + ) + } } } From 37e5efa87433f17f9975df1434558c8afee2e43e Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 12:57:35 -0400 Subject: [PATCH 31/89] Started setting up sprite animations. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 62 ++++++++++ .../vbhelper/battle/BattleSpriteManager.kt | 2 +- .../vbhelper/battle/DigimonAnimationState.kt | 113 ++++++++++++++++++ .../vbhelper/screens/BattlesScreen.kt | 32 ++++- 4 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt new file mode 100644 index 0000000..0394d85 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -0,0 +1,62 @@ +package com.github.nacabaro.vbhelper.battle + +import androidx.compose.foundation.Image +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.launch + +@Composable +fun AnimatedSpriteImage( + characterId: String, + animationType: DigimonAnimationType = DigimonAnimationType.IDLE, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + val context = LocalContext.current + val spriteManager = remember { BattleSpriteManager(context) } + val animationStateMachine = remember { DigimonAnimationStateMachine(characterId) } + val coroutineScope = rememberCoroutineScope() + + var bitmap by remember { mutableStateOf(null) } + + // Start the animation when the component is first created + LaunchedEffect(characterId) { + coroutineScope.launch { + animationStateMachine.playAnimation(DigimonAnimationType.IDLE) + } + } + + // Change animation when animationType changes + LaunchedEffect(animationType) { + coroutineScope.launch { + animationStateMachine.playAnimation(animationType) + } + } + + // Update sprite when animation state changes + LaunchedEffect(animationStateMachine.currentSpriteIndex) { + val spriteName = animationStateMachine.getCurrentSpriteName() + val atlasName = animationStateMachine.getCurrentAtlasName() + + println("Loading animated sprite: $spriteName from atlas: $atlasName") + bitmap = spriteManager.loadSprite(spriteName, atlasName) + + if (bitmap == null) { + println("Failed to load animated sprite: $spriteName from atlas: $atlasName") + } else { + println("Successfully loaded animated sprite: $spriteName from atlas: $atlasName") + } + } + + bitmap?.let { bmp -> + Image( + bitmap = bmp.asImageBitmap(), + contentDescription = "Animated Sprite: $characterId - ${animationStateMachine.currentAnimation}", + modifier = modifier, + contentScale = contentScale + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 5347c5e..316d486 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -73,7 +73,7 @@ class BattleSpriteManager(private val context: Context) { } // Load the specific sprite data file - val spriteDataFile = File(spriteBaseDir, "sprites/${spriteName}_sprite_00.json") + val spriteDataFile = File(spriteBaseDir, "sprites/${spriteName}.json") if (!spriteDataFile.exists()) { println("Sprite data file not found: ${spriteDataFile.absolutePath}") return null diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt new file mode 100644 index 0000000..bd3a9c0 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -0,0 +1,113 @@ +package com.github.nacabaro.vbhelper.battle + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay + +enum class DigimonAnimationType { + IDLE, + IDLE2, + WALK, + WALK2, + RUN, + RUN2, + WORKOUT, + WORKOUT2, + HAPPY, + SLEEP, + ATTACK, + FLEE +} + +data class AnimationState( + val type: DigimonAnimationType, + val spriteIndex: Int, // 00, 01, 02, etc. + val duration: Long = 100L, // Duration in milliseconds + val loop: Boolean = true +) + +class DigimonAnimationStateMachine( + private val characterId: String +) { + var currentAnimation by mutableStateOf(DigimonAnimationType.IDLE) + private set + + var currentSpriteIndex by mutableStateOf(0) + private set + + var isPlaying by mutableStateOf(false) + private set + + // Animation mapping - maps animation types to sprite indices + // For now, we'll assume the sprite indices 0-11 correspond to the 12 animation types + private val animationMapping = mapOf( + DigimonAnimationType.IDLE to 0, + DigimonAnimationType.IDLE2 to 1, + DigimonAnimationType.WALK to 2, + DigimonAnimationType.WALK2 to 3, + DigimonAnimationType.RUN to 4, + DigimonAnimationType.RUN2 to 5, + DigimonAnimationType.WORKOUT to 6, + DigimonAnimationType.WORKOUT2 to 7, + DigimonAnimationType.HAPPY to 8, + DigimonAnimationType.SLEEP to 9, + DigimonAnimationType.ATTACK to 10, + DigimonAnimationType.FLEE to 11 + ) + + // Animation durations for each type + private val animationDurations = mapOf( + DigimonAnimationType.IDLE to 500L, + DigimonAnimationType.IDLE2 to 500L, + DigimonAnimationType.WALK to 200L, + DigimonAnimationType.WALK2 to 200L, + DigimonAnimationType.RUN to 150L, + DigimonAnimationType.RUN2 to 150L, + DigimonAnimationType.WORKOUT to 300L, + DigimonAnimationType.WORKOUT2 to 300L, + DigimonAnimationType.HAPPY to 400L, + DigimonAnimationType.SLEEP to 1000L, + DigimonAnimationType.ATTACK to 300L, // Longer for attack animation + DigimonAnimationType.FLEE to 150L + ) + + suspend fun playAnimation(animationType: DigimonAnimationType) { + if (currentAnimation == animationType && isPlaying) { + return // Already playing this animation + } + + currentAnimation = animationType + isPlaying = true + + val spriteIndex = animationMapping[animationType] ?: 0 + currentSpriteIndex = spriteIndex + + val duration = animationDurations[animationType] ?: 100L + + // For non-looping animations like ATTACK, play once and return to IDLE + if (animationType == DigimonAnimationType.ATTACK) { + delay(duration) + playAnimation(DigimonAnimationType.IDLE) + } else { + // For looping animations, keep playing + while (isPlaying && currentAnimation == animationType) { + delay(duration) + // For now, we'll just keep the same sprite + // In the future, we could cycle through multiple sprites for each animation + } + } + } + + fun stopAnimation() { + isPlaying = false + } + + fun getCurrentSpriteName(): String { + return "${characterId}_sprite_${String.format("%02d", currentSpriteIndex)}" + } + + fun getCurrentAtlasName(): String { + return characterId + } +} \ No newline at end of file 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 3b2fdcf..8ed70b7 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 @@ -49,6 +49,8 @@ import com.github.nacabaro.vbhelper.battle.SpriteImage import com.github.nacabaro.vbhelper.battle.AttackSpriteImage import com.github.nacabaro.vbhelper.battle.SpriteFileManager import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem +import com.github.nacabaro.vbhelper.battle.DigimonAnimationType +import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage @Composable fun BattleScreen( @@ -266,9 +268,18 @@ fun PlayerBattleView( .size(80.dp), contentAlignment = Alignment.CenterStart ) { - SpriteImage( - spriteName = activeCharacter?.charaId ?: "dim011_mon01", - atlasName = activeCharacter?.charaId ?: "dim011_mon01", + // Determine animation type based on battle state + val animationType = when (battleSystem.attackPhase) { + 1 -> DigimonAnimationType.ATTACK // Player attack on player screen + 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen + 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen + 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen + else -> DigimonAnimationType.IDLE + } + + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = animationType, modifier = Modifier .size(80.dp) .scale(-1f, 1f), // Flip player Digimon horizontally @@ -473,9 +484,18 @@ fun OpponentBattleView( .size(80.dp), contentAlignment = Alignment.CenterEnd ) { - SpriteImage( - spriteName = activeCharacter?.charaId ?: "dim011_mon01", - atlasName = activeCharacter?.charaId ?: "dim011_mon01", + // Determine animation type based on battle state + val animationType = when (battleSystem.attackPhase) { + 1 -> DigimonAnimationType.IDLE // Player attack on player screen + 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen + 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen + 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen + else -> DigimonAnimationType.IDLE + } + + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = animationType, modifier = Modifier.size(80.dp), contentScale = ContentScale.Fit ) From 9f7e4528502ca89ec4ab1b1dc729484bca984fb4 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 13:00:16 -0400 Subject: [PATCH 32/89] Added Digimon idle animation. --- .../vbhelper/battle/DigimonAnimationState.kt | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index bd3a9c0..7875295 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -56,6 +56,22 @@ class DigimonAnimationStateMachine( DigimonAnimationType.FLEE to 11 ) + // Animation frame sequences - defines which frames to cycle through for each animation + private val animationFrameSequences = mapOf( + DigimonAnimationType.IDLE to listOf(0, 1), // Cycle between idle frames + DigimonAnimationType.IDLE2 to listOf(1, 0), // Alternative idle cycle + DigimonAnimationType.WALK to listOf(2, 3), // Walk animation frames + DigimonAnimationType.WALK2 to listOf(3, 2), // Alternative walk cycle + DigimonAnimationType.RUN to listOf(4, 5), // Run animation frames + DigimonAnimationType.RUN2 to listOf(5, 4), // Alternative run cycle + DigimonAnimationType.WORKOUT to listOf(6, 7), // Workout animation frames + DigimonAnimationType.WORKOUT2 to listOf(7, 6), // Alternative workout cycle + DigimonAnimationType.HAPPY to listOf(8), // Single happy frame + DigimonAnimationType.SLEEP to listOf(9), // Single sleep frame + DigimonAnimationType.ATTACK to listOf(10), // Single attack frame + DigimonAnimationType.FLEE to listOf(11) // Single flee frame + ) + // Animation durations for each type private val animationDurations = mapOf( DigimonAnimationType.IDLE to 500L, @@ -80,21 +96,21 @@ class DigimonAnimationStateMachine( currentAnimation = animationType isPlaying = true - val spriteIndex = animationMapping[animationType] ?: 0 - currentSpriteIndex = spriteIndex - + val frameSequence = animationFrameSequences[animationType] ?: listOf(0) val duration = animationDurations[animationType] ?: 100L // For non-looping animations like ATTACK, play once and return to IDLE if (animationType == DigimonAnimationType.ATTACK) { + currentSpriteIndex = frameSequence[0] delay(duration) playAnimation(DigimonAnimationType.IDLE) } else { - // For looping animations, keep playing + // For looping animations, cycle through frames + var frameIndex = 0 while (isPlaying && currentAnimation == animationType) { + currentSpriteIndex = frameSequence[frameIndex % frameSequence.size] delay(duration) - // For now, we'll just keep the same sprite - // In the future, we could cycle through multiple sprites for each animation + frameIndex++ } } } From c973030d9da4fe1be2c17f2cfbda2f4823e6c7a0 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 13:23:04 -0400 Subject: [PATCH 33/89] Fixed animation mapping. Was previously using _00, _01, _02, etc. number suffix at end of .json file. Actual sprite index is stored as the m_Name field inside this file. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 2 +- .../vbhelper/battle/DigimonAnimationState.kt | 123 +++++++++++++----- 2 files changed, 89 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt index 0394d85..64ad74c 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -17,7 +17,7 @@ fun AnimatedSpriteImage( ) { val context = LocalContext.current val spriteManager = remember { BattleSpriteManager(context) } - val animationStateMachine = remember { DigimonAnimationStateMachine(characterId) } + val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context) } val coroutineScope = rememberCoroutineScope() var bitmap by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 7875295..41141c3 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -4,6 +4,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlinx.coroutines.delay +import android.content.Context +import java.io.File +import com.google.gson.Gson enum class DigimonAnimationType { IDLE, @@ -28,7 +31,8 @@ data class AnimationState( ) class DigimonAnimationStateMachine( - private val characterId: String + private val characterId: String, + private val context: Context ) { var currentAnimation by mutableStateOf(DigimonAnimationType.IDLE) private set @@ -39,38 +43,24 @@ class DigimonAnimationStateMachine( var isPlaying by mutableStateOf(false) private set - // Animation mapping - maps animation types to sprite indices - // For now, we'll assume the sprite indices 0-11 correspond to the 12 animation types - private val animationMapping = mapOf( - DigimonAnimationType.IDLE to 0, - DigimonAnimationType.IDLE2 to 1, - DigimonAnimationType.WALK to 2, - DigimonAnimationType.WALK2 to 3, - DigimonAnimationType.RUN to 4, - DigimonAnimationType.RUN2 to 5, - DigimonAnimationType.WORKOUT to 6, - DigimonAnimationType.WORKOUT2 to 7, - DigimonAnimationType.HAPPY to 8, - DigimonAnimationType.SLEEP to 9, - DigimonAnimationType.ATTACK to 10, - DigimonAnimationType.FLEE to 11 + // Animation mapping based on m_Name values + private val mNameToAnimationType = mapOf( + "01" to DigimonAnimationType.IDLE, + "02" to DigimonAnimationType.IDLE2, + "03" to DigimonAnimationType.WALK, + "04" to DigimonAnimationType.WALK2, + "05" to DigimonAnimationType.RUN, + "06" to DigimonAnimationType.RUN2, + "07" to DigimonAnimationType.WORKOUT, + "08" to DigimonAnimationType.WORKOUT2, + "09" to DigimonAnimationType.HAPPY, + "10" to DigimonAnimationType.SLEEP, + "11" to DigimonAnimationType.ATTACK, + "12" to DigimonAnimationType.FLEE ) - // Animation frame sequences - defines which frames to cycle through for each animation - private val animationFrameSequences = mapOf( - DigimonAnimationType.IDLE to listOf(0, 1), // Cycle between idle frames - DigimonAnimationType.IDLE2 to listOf(1, 0), // Alternative idle cycle - DigimonAnimationType.WALK to listOf(2, 3), // Walk animation frames - DigimonAnimationType.WALK2 to listOf(3, 2), // Alternative walk cycle - DigimonAnimationType.RUN to listOf(4, 5), // Run animation frames - DigimonAnimationType.RUN2 to listOf(5, 4), // Alternative run cycle - DigimonAnimationType.WORKOUT to listOf(6, 7), // Workout animation frames - DigimonAnimationType.WORKOUT2 to listOf(7, 6), // Alternative workout cycle - DigimonAnimationType.HAPPY to listOf(8), // Single happy frame - DigimonAnimationType.SLEEP to listOf(9), // Single sleep frame - DigimonAnimationType.ATTACK to listOf(10), // Single attack frame - DigimonAnimationType.FLEE to listOf(11) // Single flee frame - ) + // Cache for sprite file mappings + private var spriteFileMappings: Map> = emptyMap() // Animation durations for each type private val animationDurations = mapOf( @@ -84,10 +74,65 @@ class DigimonAnimationStateMachine( DigimonAnimationType.WORKOUT2 to 300L, DigimonAnimationType.HAPPY to 400L, DigimonAnimationType.SLEEP to 1000L, - DigimonAnimationType.ATTACK to 300L, // Longer for attack animation + DigimonAnimationType.ATTACK to 300L, DigimonAnimationType.FLEE to 150L ) + init { + loadSpriteFileMappings() + } + + private fun loadSpriteFileMappings() { + try { + val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets/sprites") + val gson = Gson() + + val mappings = mutableMapOf>() + + // Initialize all animation types + DigimonAnimationType.values().forEach { animationType -> + mappings[animationType] = mutableListOf() + } + + println("Loading sprite mappings for character: $characterId") + + // Scan all sprite files for this character + val spriteFiles = spriteBaseDir.listFiles { file -> + file.name.startsWith("${characterId}_sprite_") && file.name.endsWith(".json") + } + + println("Found ${spriteFiles?.size ?: 0} sprite files for $characterId") + + spriteFiles?.forEach { spriteFile -> + println("Processing sprite file: ${spriteFile.name}") + val spriteDataJson = spriteFile.readText() + val spriteData = gson.fromJson(spriteDataJson, SpriteData::class.java) + + println(" m_Name: ${spriteData.m_Name}") + + // Get the animation type from m_Name + val animationType = mNameToAnimationType[spriteData.m_Name] + if (animationType != null) { + // Extract the sprite index from filename (e.g., "dim000_mon01_sprite_00.json" -> "00") + val spriteIndex = spriteFile.name.substringAfter("_sprite_").substringBefore(".json") + mappings[animationType]?.add(spriteIndex) + println(" Mapped to animation type: $animationType with sprite index: $spriteIndex") + } else { + println(" Unknown m_Name: ${spriteData.m_Name}") + } + } + + // Convert to immutable map + spriteFileMappings = mappings.mapValues { it.value.sorted() } + + println("Final sprite mappings for $characterId: $spriteFileMappings") + + } catch (e: Exception) { + println("Error loading sprite file mappings: ${e.message}") + e.printStackTrace() + } + } + suspend fun playAnimation(animationType: DigimonAnimationType) { if (currentAnimation == animationType && isPlaying) { return // Already playing this animation @@ -96,19 +141,27 @@ class DigimonAnimationStateMachine( currentAnimation = animationType isPlaying = true - val frameSequence = animationFrameSequences[animationType] ?: listOf(0) + val frameSequence = spriteFileMappings[animationType] ?: listOf("00") val duration = animationDurations[animationType] ?: 100L + // Ensure we have at least one frame + if (frameSequence.isEmpty()) { + println("Warning: No sprite files found for animation type $animationType") + currentSpriteIndex = 0 + return + } + // For non-looping animations like ATTACK, play once and return to IDLE if (animationType == DigimonAnimationType.ATTACK) { - currentSpriteIndex = frameSequence[0] + currentSpriteIndex = frameSequence.firstOrNull()?.toIntOrNull() ?: 0 delay(duration) playAnimation(DigimonAnimationType.IDLE) } else { // For looping animations, cycle through frames var frameIndex = 0 while (isPlaying && currentAnimation == animationType) { - currentSpriteIndex = frameSequence[frameIndex % frameSequence.size] + val spriteIndex = frameSequence[frameIndex % frameSequence.size] + currentSpriteIndex = spriteIndex.toIntOrNull() ?: 0 delay(duration) frameIndex++ } From 3687ff2c21d846d154606e662ca512d8ee1ac2e3 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 13:26:39 -0400 Subject: [PATCH 34/89] Updated idle animation to use 2 sprites instead of just 1. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 8 +++-- .../vbhelper/battle/DigimonAnimationState.kt | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt index 64ad74c..65cb652 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -25,14 +25,18 @@ fun AnimatedSpriteImage( // Start the animation when the component is first created LaunchedEffect(characterId) { coroutineScope.launch { - animationStateMachine.playAnimation(DigimonAnimationType.IDLE) + animationStateMachine.playIdleAnimation() } } // Change animation when animationType changes LaunchedEffect(animationType) { coroutineScope.launch { - animationStateMachine.playAnimation(animationType) + if (animationType == DigimonAnimationType.IDLE) { + animationStateMachine.playIdleAnimation() + } else { + animationStateMachine.playAnimation(animationType) + } } } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 41141c3..0c5d9a5 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -168,6 +168,40 @@ class DigimonAnimationStateMachine( } } + // Special method for idle animation that cycles between IDLE and IDLE2 + suspend fun playIdleAnimation() { + if (currentAnimation == DigimonAnimationType.IDLE && isPlaying) { + return // Already playing idle animation + } + + currentAnimation = DigimonAnimationType.IDLE + isPlaying = true + + // Get both IDLE and IDLE2 frames + val idleFrames = spriteFileMappings[DigimonAnimationType.IDLE] ?: listOf("00") + val idle2Frames = spriteFileMappings[DigimonAnimationType.IDLE2] ?: listOf("01") + + // Combine frames for cycling idle animation + val combinedFrames = (idleFrames + idle2Frames).distinct() + + if (combinedFrames.isEmpty()) { + println("Warning: No idle sprite files found") + currentSpriteIndex = 0 + return + } + + val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L + + // Cycle through idle frames + var frameIndex = 0 + while (isPlaying && currentAnimation == DigimonAnimationType.IDLE) { + val spriteIndex = combinedFrames[frameIndex % combinedFrames.size] + currentSpriteIndex = spriteIndex.toIntOrNull() ?: 0 + delay(duration) + frameIndex++ + } + } + fun stopAnimation() { isPlaying = false } From 615fb852041241b14b848f8ffae8bb176d8e7631 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 14:13:26 -0400 Subject: [PATCH 35/89] Added sprite test button to battle. --- .../vbhelper/battle/DigimonAnimationState.kt | 6 +- .../vbhelper/screens/BattlesScreen.kt | 157 ++++++++++++++++-- 2 files changed, 146 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 0c5d9a5..2042dbd 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -176,10 +176,10 @@ class DigimonAnimationStateMachine( currentAnimation = DigimonAnimationType.IDLE isPlaying = true - - // Get both IDLE and IDLE2 frames + val idleFrames = spriteFileMappings[DigimonAnimationType.IDLE] ?: listOf("00") - val idle2Frames = spriteFileMappings[DigimonAnimationType.IDLE2] ?: listOf("01") + + val idle2Frames = spriteFileMappings[DigimonAnimationType.HAPPY] ?: listOf("08") // Combine frames for cycling idle animation val combinedFrames = (idleFrames + idle2Frames).distinct() 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 8ed70b7..5f14e58 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 @@ -52,6 +52,7 @@ import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage + @Composable fun BattleScreen( stage: String, @@ -592,6 +593,13 @@ fun BattlesScreen() { var expanded by remember { mutableStateOf(false) } var selectedStage by remember { mutableStateOf("") } var currentStage by remember { mutableStateOf("rookie") } + + // Sprite animation tester state + var showSpriteTester by remember { mutableStateOf(false) } + var dimId by remember { mutableStateOf("") } + var monId by remember { mutableStateOf("") } + var currentTestAnimation by remember { mutableStateOf(DigimonAnimationType.IDLE) } + var testCharacterId by remember { mutableStateOf("") } // Create hardcoded character lists for each stage val rookieCharacters = listOf( @@ -775,6 +783,118 @@ fun BattlesScreen() { } } } + + val spriteTester = @Composable { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + Text("Sprite Animation Tester", fontSize = 20.sp, fontWeight = FontWeight.Bold) + + Spacer(modifier = Modifier.height(16.dp)) + + // DIM ID input + OutlinedTextField( + value = dimId, + onValueChange = { dimId = it }, + label = { Text("DIM ID (e.g., 012)") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Mon ID input + OutlinedTextField( + value = monId, + onValueChange = { monId = it }, + label = { Text("Mon ID (e.g., 03)") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Load sprite button + Button( + onClick = { + if (dimId.isNotEmpty() && monId.isNotEmpty()) { + testCharacterId = "dim${dimId}_mon${monId}" + println("Testing sprite for: $testCharacterId") + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Load Sprite") + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Animation test buttons + if (testCharacterId.isNotEmpty()) { + Text("Animation States:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + + // Create a grid of animation buttons + val animationTypes = listOf( + DigimonAnimationType.IDLE to "IDLE", + DigimonAnimationType.IDLE2 to "IDLE2", + DigimonAnimationType.WALK to "WALK", + DigimonAnimationType.WALK2 to "WALK2", + DigimonAnimationType.RUN to "RUN", + DigimonAnimationType.RUN2 to "RUN2", + DigimonAnimationType.WORKOUT to "WORKOUT", + DigimonAnimationType.WORKOUT2 to "WORKOUT2", + DigimonAnimationType.HAPPY to "HAPPY", + DigimonAnimationType.SLEEP to "SLEEP", + DigimonAnimationType.ATTACK to "ATTACK", + DigimonAnimationType.FLEE to "FLEE" + ) + + // Display sprite + AnimatedSpriteImage( + characterId = testCharacterId, + animationType = currentTestAnimation, + modifier = Modifier.size(120.dp), + contentScale = ContentScale.Fit + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Animation buttons in a grid + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Create rows of 3 buttons each + animationTypes.chunked(3).forEach { row -> + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + row.forEach { (animationType, label) -> + Button( + onClick = { currentTestAnimation = animationType }, + colors = ButtonDefaults.buttonColors( + containerColor = if (currentTestAnimation == animationType) Color.Blue else Color.Gray + ), + modifier = Modifier.weight(1f) + ) { + Text(label, fontSize = 12.sp) + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { showSpriteTester = false }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Back to Main") + } + } + } Scaffold ( topBar = { @@ -792,21 +912,30 @@ fun BattlesScreen() { ) { when (currentView) { "main" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - rookieButton() - championButton() - ultimateButton() - megaButton() - Button( - onClick = { - val spriteFileManager = SpriteFileManager(context) - spriteFileManager.clearSpriteFiles() - println("Sprite files cleared!") - } + if (showSpriteTester) { + spriteTester() + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Clear Sprite Files") + rookieButton() + championButton() + ultimateButton() + megaButton() + Button( + onClick = { showSpriteTester = true } + ) { + Text("Sprite Animation Tester") + } + Button( + onClick = { + val spriteFileManager = SpriteFileManager(context) + spriteFileManager.clearSpriteFiles() + println("Sprite files cleared!") + } + ) { + Text("Clear Sprite Files") + } } } } From fb0935082525c9c120ea9d39dccbf934f32a18ed Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 4 Aug 2025 14:26:24 -0400 Subject: [PATCH 36/89] Created sprite and animation tester menu. --- .../vbhelper/screens/BattlesScreen.kt | 167 ++++++++++++------ .../nacabhelper/screens/BattlesScreen.kt | 1 + 2 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt 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 5f14e58..10afc2f 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 @@ -51,6 +51,8 @@ import com.github.nacabaro.vbhelper.battle.SpriteFileManager import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items @Composable @@ -596,6 +598,7 @@ fun BattlesScreen() { // Sprite animation tester state var showSpriteTester by remember { mutableStateOf(false) } + var spriteTesterView by remember { mutableStateOf("entry") } // "entry" or "testing" var dimId by remember { mutableStateOf("") } var monId by remember { mutableStateOf("") } var currentTestAnimation by remember { mutableStateOf(DigimonAnimationType.IDLE) } @@ -784,7 +787,7 @@ fun BattlesScreen() { } } - val spriteTester = @Composable { + val spriteTesterEntry = @Composable { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp) @@ -819,6 +822,7 @@ fun BattlesScreen() { if (dimId.isNotEmpty() && monId.isNotEmpty()) { testCharacterId = "dim${dimId}_mon${monId}" println("Testing sprite for: $testCharacterId") + spriteTesterView = "testing" } }, modifier = Modifier.fillMaxWidth() @@ -828,57 +832,76 @@ fun BattlesScreen() { Spacer(modifier = Modifier.height(16.dp)) - // Animation test buttons - if (testCharacterId.isNotEmpty()) { - Text("Animation States:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - - // Create a grid of animation buttons - val animationTypes = listOf( - DigimonAnimationType.IDLE to "IDLE", - DigimonAnimationType.IDLE2 to "IDLE2", - DigimonAnimationType.WALK to "WALK", - DigimonAnimationType.WALK2 to "WALK2", - DigimonAnimationType.RUN to "RUN", - DigimonAnimationType.RUN2 to "RUN2", - DigimonAnimationType.WORKOUT to "WORKOUT", - DigimonAnimationType.WORKOUT2 to "WORKOUT2", - DigimonAnimationType.HAPPY to "HAPPY", - DigimonAnimationType.SLEEP to "SLEEP", - DigimonAnimationType.ATTACK to "ATTACK", - DigimonAnimationType.FLEE to "FLEE" - ) - - // Display sprite - AnimatedSpriteImage( - characterId = testCharacterId, - animationType = currentTestAnimation, - modifier = Modifier.size(120.dp), - contentScale = ContentScale.Fit - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Animation buttons in a grid - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Create rows of 3 buttons each - animationTypes.chunked(3).forEach { row -> - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - row.forEach { (animationType, label) -> - Button( - onClick = { currentTestAnimation = animationType }, - colors = ButtonDefaults.buttonColors( - containerColor = if (currentTestAnimation == animationType) Color.Blue else Color.Gray - ), - modifier = Modifier.weight(1f) - ) { - Text(label, fontSize = 12.sp) - } + Button( + onClick = { showSpriteTester = false }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Back to Main") + } + } + } + + val spriteTesterTesting = @Composable { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + Text("Sprite Animation Testing", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Text("Character: $testCharacterId", fontSize = 14.sp, color = Color.Gray) + + Spacer(modifier = Modifier.height(16.dp)) + + // Display sprite + AnimatedSpriteImage( + characterId = testCharacterId, + animationType = currentTestAnimation, + modifier = Modifier.size(120.dp), + contentScale = ContentScale.Fit + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Animation buttons in a grid + Text("Animation Buttons:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + + val animationTypes = listOf( + DigimonAnimationType.IDLE to "IDLE", + DigimonAnimationType.IDLE2 to "IDLE2", + DigimonAnimationType.WALK to "WALK", + DigimonAnimationType.WALK2 to "WALK2", + DigimonAnimationType.RUN to "RUN", + DigimonAnimationType.RUN2 to "RUN2", + DigimonAnimationType.WORKOUT to "WORKOUT", + DigimonAnimationType.WORKOUT2 to "WORKOUT2", + DigimonAnimationType.HAPPY to "HAPPY", + DigimonAnimationType.SLEEP to "SLEEP", + DigimonAnimationType.ATTACK to "ATTACK", + DigimonAnimationType.FLEE to "FLEE" + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Create rows of 3 buttons each + animationTypes.chunked(3).forEach { row -> + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + row.forEach { (animationType, label) -> + Button( + onClick = { + currentTestAnimation = animationType + println("Switched to animation: $label") + }, + colors = ButtonDefaults.buttonColors( + containerColor = if (currentTestAnimation == animationType) Color.Blue else Color.Gray + ), + modifier = Modifier.weight(1f), + contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 4.dp, horizontal = 2.dp) + ) { + Text(label, fontSize = 10.sp) } } } @@ -887,11 +910,30 @@ fun BattlesScreen() { Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { showSpriteTester = false }, + // Navigation buttons + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Back to Main") + Button( + onClick = { spriteTesterView = "entry" }, + modifier = Modifier.weight(1f) + ) { + Text("Back to Entry") + } + + Button( + onClick = { + showSpriteTester = false + spriteTesterView = "entry" + testCharacterId = "" + dimId = "" + monId = "" + }, + modifier = Modifier.weight(1f) + ) { + Text("Back to Main") + } } } } @@ -913,7 +955,11 @@ fun BattlesScreen() { when (currentView) { "main" -> { if (showSpriteTester) { - spriteTester() + when (spriteTesterView) { + "entry" -> spriteTesterEntry() + "testing" -> spriteTesterTesting() + else -> spriteTesterEntry() + } } else { Column( horizontalAlignment = Alignment.CenterHorizontally @@ -923,7 +969,14 @@ fun BattlesScreen() { ultimateButton() megaButton() Button( - onClick = { showSpriteTester = true } + onClick = { + showSpriteTester = true + spriteTesterView = "entry" + testCharacterId = "" + dimId = "" + monId = "" + currentTestAnimation = DigimonAnimationType.IDLE + } ) { Text("Sprite Animation Tester") } diff --git a/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/app/src/main/java/com/github/nacabhelper/screens/BattlesScreen.kt @@ -0,0 +1 @@ + \ No newline at end of file From 71ba5e0207a99f0d384f4762be6416f8d69f8700 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 06:54:55 -0400 Subject: [PATCH 37/89] Fixed atk sprite import bug. Readjusted sprites to load from individual files instead of full spritesheets. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 25 ++- .../vbhelper/battle/AttackSpriteManager.kt | 4 +- .../vbhelper/battle/DigimonAnimationState.kt | 134 ++++--------- .../battle/IndividualSpriteManager.kt | 142 ++++++++++++++ .../vbhelper/battle/SpriteFileManager.kt | 180 ++++++++++++++---- .../nacabaro/vbhelper/battle/SpriteImage.kt | 19 +- .../vbhelper/screens/BattlesScreen.kt | 15 +- 7 files changed, 356 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt index 65cb652..e8c2049 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -13,15 +13,23 @@ fun AnimatedSpriteImage( characterId: String, animationType: DigimonAnimationType = DigimonAnimationType.IDLE, modifier: Modifier = Modifier, - contentScale: ContentScale = ContentScale.Fit + contentScale: ContentScale = ContentScale.Fit, + reloadMappings: Boolean = false ) { val context = LocalContext.current - val spriteManager = remember { BattleSpriteManager(context) } + val spriteManager = remember { IndividualSpriteManager(context) } val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context) } val coroutineScope = rememberCoroutineScope() var bitmap by remember { mutableStateOf(null) } + // Reload mappings when reloadMappings parameter changes + LaunchedEffect(reloadMappings) { + if (reloadMappings) { + animationStateMachine.reloadMappings() + } + } + // Start the animation when the component is first created LaunchedEffect(characterId) { coroutineScope.launch { @@ -41,17 +49,16 @@ fun AnimatedSpriteImage( } // Update sprite when animation state changes - LaunchedEffect(animationStateMachine.currentSpriteIndex) { - val spriteName = animationStateMachine.getCurrentSpriteName() - val atlasName = animationStateMachine.getCurrentAtlasName() + LaunchedEffect(animationStateMachine.currentFrameNumber) { + val frameNumber = animationStateMachine.getCurrentFrame() - println("Loading animated sprite: $spriteName from atlas: $atlasName") - bitmap = spriteManager.loadSprite(spriteName, atlasName) + println("Loading animated sprite frame: $frameNumber for character: $characterId") + bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber) if (bitmap == null) { - println("Failed to load animated sprite: $spriteName from atlas: $atlasName") + println("Failed to load animated sprite frame: $frameNumber for character: $characterId") } else { - println("Successfully loaded animated sprite: $spriteName from atlas: $atlasName") + println("Successfully loaded animated sprite frame: $frameNumber for character: $characterId") } } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt index d44bb43..064350b 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -31,8 +31,8 @@ class AttackSpriteManager(private val context: Context) { private val gson = Gson() private val characterDataCache = mutableMapOf() - // Base path for attack textures - private val attackTexturesPath = "battle_sprites/extracted_assets/extracted_atksprites" + // Base path for attack textures (updated for new folder structure) + private val attackTexturesPath = "battle_sprites/extracted_atksprites" fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 2042dbd..77028d3 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.setValue import kotlinx.coroutines.delay import android.content.Context import java.io.File -import com.google.gson.Gson enum class DigimonAnimationType { IDLE, @@ -25,7 +24,7 @@ enum class DigimonAnimationType { data class AnimationState( val type: DigimonAnimationType, - val spriteIndex: Int, // 00, 01, 02, etc. + val frameNumber: Int, // 1-12 for individual PNG files val duration: Long = 100L, // Duration in milliseconds val loop: Boolean = true ) @@ -37,30 +36,31 @@ class DigimonAnimationStateMachine( var currentAnimation by mutableStateOf(DigimonAnimationType.IDLE) private set - var currentSpriteIndex by mutableStateOf(0) + var currentFrameNumber by mutableStateOf(1) private set var isPlaying by mutableStateOf(false) private set - // Animation mapping based on m_Name values - private val mNameToAnimationType = mapOf( - "01" to DigimonAnimationType.IDLE, - "02" to DigimonAnimationType.IDLE2, - "03" to DigimonAnimationType.WALK, - "04" to DigimonAnimationType.WALK2, - "05" to DigimonAnimationType.RUN, - "06" to DigimonAnimationType.RUN2, - "07" to DigimonAnimationType.WORKOUT, - "08" to DigimonAnimationType.WORKOUT2, - "09" to DigimonAnimationType.HAPPY, - "10" to DigimonAnimationType.SLEEP, - "11" to DigimonAnimationType.ATTACK, - "12" to DigimonAnimationType.FLEE + // Direct mapping of frame numbers (1-12) to animation types + // This is based on the standard Digimon sprite frame order + private val frameToAnimationType = mapOf( + 1 to DigimonAnimationType.IDLE, + 2 to DigimonAnimationType.IDLE2, + 3 to DigimonAnimationType.WALK, + 4 to DigimonAnimationType.WALK2, + 5 to DigimonAnimationType.RUN, + 6 to DigimonAnimationType.RUN2, + 7 to DigimonAnimationType.WORKOUT, + 8 to DigimonAnimationType.WORKOUT2, + 9 to DigimonAnimationType.HAPPY, + 10 to DigimonAnimationType.SLEEP, + 11 to DigimonAnimationType.ATTACK, + 12 to DigimonAnimationType.FLEE ) - // Cache for sprite file mappings - private var spriteFileMappings: Map> = emptyMap() + // Reverse mapping for getting frame numbers for each animation type + private val animationTypeToFrames = frameToAnimationType.entries.groupBy({ it.value }, { it.key }) // Animation durations for each type private val animationDurations = mapOf( @@ -79,58 +79,8 @@ class DigimonAnimationStateMachine( ) init { - loadSpriteFileMappings() - } - - private fun loadSpriteFileMappings() { - try { - val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets/sprites") - val gson = Gson() - - val mappings = mutableMapOf>() - - // Initialize all animation types - DigimonAnimationType.values().forEach { animationType -> - mappings[animationType] = mutableListOf() - } - - println("Loading sprite mappings for character: $characterId") - - // Scan all sprite files for this character - val spriteFiles = spriteBaseDir.listFiles { file -> - file.name.startsWith("${characterId}_sprite_") && file.name.endsWith(".json") - } - - println("Found ${spriteFiles?.size ?: 0} sprite files for $characterId") - - spriteFiles?.forEach { spriteFile -> - println("Processing sprite file: ${spriteFile.name}") - val spriteDataJson = spriteFile.readText() - val spriteData = gson.fromJson(spriteDataJson, SpriteData::class.java) - - println(" m_Name: ${spriteData.m_Name}") - - // Get the animation type from m_Name - val animationType = mNameToAnimationType[spriteData.m_Name] - if (animationType != null) { - // Extract the sprite index from filename (e.g., "dim000_mon01_sprite_00.json" -> "00") - val spriteIndex = spriteFile.name.substringAfter("_sprite_").substringBefore(".json") - mappings[animationType]?.add(spriteIndex) - println(" Mapped to animation type: $animationType with sprite index: $spriteIndex") - } else { - println(" Unknown m_Name: ${spriteData.m_Name}") - } - } - - // Convert to immutable map - spriteFileMappings = mappings.mapValues { it.value.sorted() } - - println("Final sprite mappings for $characterId: $spriteFileMappings") - - } catch (e: Exception) { - println("Error loading sprite file mappings: ${e.message}") - e.printStackTrace() - } + println("Initialized DigimonAnimationStateMachine for character: $characterId") + println("Available animation types: ${animationTypeToFrames.keys}") } suspend fun playAnimation(animationType: DigimonAnimationType) { @@ -141,27 +91,22 @@ class DigimonAnimationStateMachine( currentAnimation = animationType isPlaying = true - val frameSequence = spriteFileMappings[animationType] ?: listOf("00") + val frameNumbers = animationTypeToFrames[animationType] ?: listOf(1) val duration = animationDurations[animationType] ?: 100L - // Ensure we have at least one frame - if (frameSequence.isEmpty()) { - println("Warning: No sprite files found for animation type $animationType") - currentSpriteIndex = 0 - return - } + println("Playing animation: $animationType with frames: $frameNumbers") // For non-looping animations like ATTACK, play once and return to IDLE if (animationType == DigimonAnimationType.ATTACK) { - currentSpriteIndex = frameSequence.firstOrNull()?.toIntOrNull() ?: 0 + currentFrameNumber = frameNumbers.firstOrNull() ?: 1 delay(duration) playAnimation(DigimonAnimationType.IDLE) } else { // For looping animations, cycle through frames var frameIndex = 0 while (isPlaying && currentAnimation == animationType) { - val spriteIndex = frameSequence[frameIndex % frameSequence.size] - currentSpriteIndex = spriteIndex.toIntOrNull() ?: 0 + val frameNumber = frameNumbers[frameIndex % frameNumbers.size] + currentFrameNumber = frameNumber delay(duration) frameIndex++ } @@ -177,26 +122,21 @@ class DigimonAnimationStateMachine( currentAnimation = DigimonAnimationType.IDLE isPlaying = true - val idleFrames = spriteFileMappings[DigimonAnimationType.IDLE] ?: listOf("00") - - val idle2Frames = spriteFileMappings[DigimonAnimationType.HAPPY] ?: listOf("08") + val idleFrames = animationTypeToFrames[DigimonAnimationType.IDLE] ?: listOf(1) + val idle2Frames = animationTypeToFrames[DigimonAnimationType.IDLE2] ?: listOf(2) // Combine frames for cycling idle animation val combinedFrames = (idleFrames + idle2Frames).distinct() - if (combinedFrames.isEmpty()) { - println("Warning: No idle sprite files found") - currentSpriteIndex = 0 - return - } + println("Playing idle animation with frames: $combinedFrames") val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L // Cycle through idle frames var frameIndex = 0 while (isPlaying && currentAnimation == DigimonAnimationType.IDLE) { - val spriteIndex = combinedFrames[frameIndex % combinedFrames.size] - currentSpriteIndex = spriteIndex.toIntOrNull() ?: 0 + val frameNumber = combinedFrames[frameIndex % combinedFrames.size] + currentFrameNumber = frameNumber delay(duration) frameIndex++ } @@ -206,11 +146,17 @@ class DigimonAnimationStateMachine( isPlaying = false } - fun getCurrentSpriteName(): String { - return "${characterId}_sprite_${String.format("%02d", currentSpriteIndex)}" + fun getCurrentFrame(): Int { + return currentFrameNumber } - fun getCurrentAtlasName(): String { + fun getCurrentCharacterId(): String { return characterId } + + // Method to reload mappings (useful for testing) + fun reloadMappings() { + println("Reloading mappings for character: $characterId") + // No need to reload since we use direct frame mapping + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt new file mode 100644 index 0000000..f4b0d25 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt @@ -0,0 +1,142 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.File + +class IndividualSpriteManager(private val context: Context) { + private val spriteCache = mutableMapOf() + + // Base directory where individual sprite PNGs are stored + private val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets/sprites") + + /** + * Load a specific sprite frame for a character + * @param characterId The character ID (e.g., "dim012_mon03") + * @param frameNumber The frame number (1-12) + * @return Bitmap of the sprite frame, or null if not found + */ + fun loadSpriteFrame(characterId: String, frameNumber: Int): Bitmap? { + val cacheKey = "${characterId}_frame_${frameNumber}" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + // Debug: Check if base directory exists + if (!spriteBaseDir.exists()) { + println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}") + return null + } + + try { + // Construct the sprite file path + val spriteFileName = "${characterId}_${String.format("%02d", frameNumber)}.png" + val spriteFile = File(spriteBaseDir, "$characterId/$spriteFileName") + + if (!spriteFile.exists()) { + println("Sprite file not found: ${spriteFile.absolutePath}") + return null + } + + // Load the PNG file directly + val bitmap = BitmapFactory.decodeFile(spriteFile.absolutePath) + if (bitmap == null) { + println("Failed to decode sprite file: ${spriteFile.absolutePath}") + return null + } + + println("Successfully loaded sprite frame: $spriteFileName (${bitmap.width}x${bitmap.height})") + + // Cache the result + spriteCache[cacheKey] = bitmap + + return bitmap + + } catch (e: Exception) { + println("Error loading sprite frame: ${e.message}") + e.printStackTrace() + return null + } + } + + /** + * Get all available sprite frames for a character + * @param characterId The character ID + * @return List of frame numbers (1-12) that exist for this character + */ + fun getAvailableFrames(characterId: String): List { + try { + val characterDir = File(spriteBaseDir, characterId) + if (!characterDir.exists()) { + println("Character directory not found: ${characterDir.absolutePath}") + return emptyList() + } + + val spriteFiles = characterDir.listFiles { file -> + file.name.startsWith("${characterId}_") && file.name.endsWith(".png") + } ?: emptyArray() + + return spriteFiles.mapNotNull { file -> + // Extract frame number from filename (e.g., "dim012_mon03_01.png" -> 1) + val frameNumberStr = file.name.substringAfter("_").substringBefore(".png") + frameNumberStr.toIntOrNull() + }.sorted() + + } catch (e: Exception) { + println("Error getting available frames: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Get all available characters + * @return List of character IDs that have sprite directories + */ + fun getAvailableCharacters(): List { + try { + if (!spriteBaseDir.exists()) { + return emptyList() + } + + val characterDirs = spriteBaseDir.listFiles { file -> + file.isDirectory && file.name.matches(Regex("dim\\d+_mon\\d+.*")) + } ?: emptyArray() + + return characterDirs.map { it.name }.sorted() + + } catch (e: Exception) { + println("Error getting available characters: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Clear the sprite cache + */ + fun clearCache() { + spriteCache.clear() + } + + /** + * Check if a character has sprite files + * @param characterId The character ID to check + * @return true if the character has sprite files, false otherwise + */ + fun hasCharacterSprites(characterId: String): Boolean { + val characterDir = File(spriteBaseDir, characterId) + if (!characterDir.exists()) { + return false + } + + val spriteFiles = characterDir.listFiles { file -> + file.name.startsWith("${characterId}_") && file.name.endsWith(".png") + } ?: emptyArray() + + return spriteFiles.isNotEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 0298f3d..5dd3699 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -9,26 +9,90 @@ class SpriteFileManager(private val context: Context) { fun copySpriteFilesToInternalStorage() { try { - // Create the base directory for extracted_assets - val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") - if (!extractedAssetsDir.exists()) { - extractedAssetsDir.mkdirs() + println("Starting sprite file copy process...") + + // Debug: List what's in the assets directory + val assetManager = context.assets + val battleSpritesFiles = assetManager.list("battle_sprites") + println("battle_sprites directory in assets contains: ${battleSpritesFiles?.joinToString(", ")}") + + val extractedAssetsFiles = assetManager.list("battle_sprites/extracted_assets") + println("battle_sprites/extracted_assets directory in assets contains: ${extractedAssetsFiles?.joinToString(", ")}") + + // Check specifically for extracted_atksprites in assets (now directly under battle_sprites) + val atkspritesInAssets = assetManager.list("battle_sprites/extracted_atksprites") + println("extracted_atksprites in assets contains: ${atkspritesInAssets?.size ?: 0} files") + if (atkspritesInAssets != null && atkspritesInAssets.isNotEmpty()) { + println("First few attack files in assets: ${atkspritesInAssets.take(5).joinToString(", ")}") } - // Create the base directory for extracted_digimon_stats - val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") - if (!extractedStatsDir.exists()) { - extractedStatsDir.mkdirs() + // Check for extracted_battlebgs in assets (now directly under battle_sprites) + val battlebgsInAssets = assetManager.list("battle_sprites/extracted_battlebgs") + println("extracted_battlebgs in assets contains: ${battlebgsInAssets?.size ?: 0} files") + if (battlebgsInAssets != null && battlebgsInAssets.isNotEmpty()) { + println("First few battle background files in assets: ${battlebgsInAssets.take(5).joinToString(", ")}") } - // Copy extracted_assets files from assets to internal storage - copyAssetDirectory("battle_sprites/extracted_assets", extractedAssetsDir) + // Try to list all possible subdirectories in battle_sprites + println("Checking all possible subdirectories in battle_sprites...") + battleSpritesFiles?.forEach { subdir -> + try { + val subdirFiles = assetManager.list("battle_sprites/$subdir") + println(" $subdir contains: ${subdirFiles?.size ?: 0} files") + if (subdirFiles != null && subdirFiles.isNotEmpty()) { + println(" First few files: ${subdirFiles.take(3).joinToString(", ")}") + } + } catch (e: Exception) { + println(" Error listing $subdir: ${e.message}") + } + } - // Copy extracted_digimon_stats files from assets to internal storage - copyAssetDirectory("battle_sprites/extracted_digimon_stats", extractedStatsDir) + // Create the base directory for battle_sprites + val battleSpritesDir = File(context.filesDir, "battle_sprites") + if (!battleSpritesDir.exists()) { + battleSpritesDir.mkdirs() + println("Created battle_sprites directory: ${battleSpritesDir.absolutePath}") + } else { + println("battle_sprites directory already exists: ${battleSpritesDir.absolutePath}") + } - println("Sprite files copied successfully to: ${extractedAssetsDir.absolutePath}") - println("Stats files copied successfully to: ${extractedStatsDir.absolutePath}") + // Copy all subdirectories from battle_sprites assets to internal storage + println("Copying all battle_sprites subdirectories...") + battleSpritesFiles?.forEach { subdir -> + val sourcePath = "battle_sprites/$subdir" + val targetDir = File(battleSpritesDir, subdir) + println("Copying $sourcePath to ${targetDir.absolutePath}") + copyAssetDirectory(sourcePath, targetDir) + } + + println("Sprite files copied successfully to: ${battleSpritesDir.absolutePath}") + + // Verify that attack sprites were copied + val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") + if (atkspritesDir.exists()) { + val attackFiles = atkspritesDir.listFiles() + println("Attack sprites directory exists with ${attackFiles?.size ?: 0} files") + if (attackFiles != null && attackFiles.isNotEmpty()) { + println("First few attack files: ${attackFiles.take(5).map { it.name }}") + } + } else { + println("WARNING: extracted_atksprites directory does not exist!") + // List what's actually in the battle_sprites directory + val battleSpritesContents = battleSpritesDir.listFiles() + println("battle_sprites directory contains: ${battleSpritesContents?.map { it.name }?.joinToString(", ")}") + } + + // Verify that battle backgrounds were copied + val battlebgsDir = File(battleSpritesDir, "extracted_battlebgs") + if (battlebgsDir.exists()) { + val bgFiles = battlebgsDir.listFiles() + println("Battle backgrounds directory exists with ${bgFiles?.size ?: 0} files") + if (bgFiles != null && bgFiles.isNotEmpty()) { + println("First few battle background files: ${bgFiles.take(5).map { it.name }}") + } + } else { + println("WARNING: extracted_battlebgs directory does not exist!") + } } catch (e: Exception) { println("Error copying sprite files: ${e.message}") @@ -41,6 +105,9 @@ class SpriteFileManager(private val context: Context) { val assetManager = context.assets val files = assetManager.list(assetPath) ?: return + println("Copying asset directory: $assetPath (${files.size} items)") + println("Files found: ${files.joinToString(", ")}") + for (file in files) { val assetFilePath = if (assetPath.isEmpty()) file else "$assetPath/$file" val targetFile = File(targetDir, file) @@ -50,21 +117,50 @@ class SpriteFileManager(private val context: Context) { targetFile.parentFile!!.mkdirs() } - // Check if it's a directory - val subFiles = assetManager.list(assetFilePath) - if (subFiles != null && subFiles.isNotEmpty()) { - // It's a directory, create it and copy contents - if (!targetFile.exists()) { - targetFile.mkdirs() + // Check if it's a directory by trying to list its contents + try { + val subFiles = assetManager.list(assetFilePath) + if (subFiles != null && subFiles.isNotEmpty()) { + // It's a directory, create it and copy contents + println("Copying subdirectory: $assetFilePath (${subFiles.size} files)") + if (!targetFile.exists()) { + targetFile.mkdirs() + } + copyAssetDirectory(assetFilePath, targetFile) + } else { + // It's a file, copy it + copyAssetFile(assetFilePath, targetFile) } - copyAssetDirectory(assetFilePath, targetFile) - } else { - // It's a file, copy it + } catch (e: Exception) { + // If we can't list contents, it's probably a file + println("Treating $assetFilePath as file (could not list contents)") copyAssetFile(assetFilePath, targetFile) } } + + // Special handling for extracted_atksprites - try to copy it directly if it wasn't found + if (assetPath == "battle_sprites/extracted_assets") { + println("Special handling: Checking for extracted_atksprites directory...") + try { + val atkspritesFiles = assetManager.list("battle_sprites/extracted_assets/extracted_atksprites") + if (atkspritesFiles != null && atkspritesFiles.isNotEmpty()) { + println("Found extracted_atksprites with ${atkspritesFiles.size} files") + val atkspritesDir = File(targetDir, "extracted_atksprites") + if (!atkspritesDir.exists()) { + atkspritesDir.mkdirs() + } + copyAssetDirectory("battle_sprites/extracted_assets/extracted_atksprites", atkspritesDir) + } else { + println("extracted_atksprites directory not found in assets") + } + } catch (e: Exception) { + println("Error checking extracted_atksprites: ${e.message}") + } + } + } catch (e: Exception) { println("Error copying asset directory $assetPath: ${e.message}") + e.printStackTrace() } } @@ -84,34 +180,34 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") - val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") + val battleSpritesDir = File(context.filesDir, "battle_sprites") + val extractedAssetsDir = File(battleSpritesDir, "extracted_assets") + val extractedStatsDir = File(battleSpritesDir, "extracted_digimon_stats") + val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") + val battlebgsDir = File(battleSpritesDir, "extracted_battlebgs") + val battleSpritesExist = battleSpritesDir.exists() && battleSpritesDir.listFiles()?.isNotEmpty() == true val assetsExist = extractedAssetsDir.exists() && extractedAssetsDir.listFiles()?.isNotEmpty() == true val statsExist = extractedStatsDir.exists() && extractedStatsDir.listFiles()?.isNotEmpty() == true + val atkspritesExist = atkspritesDir.exists() && atkspritesDir.listFiles()?.isNotEmpty() == true + val battlebgsExist = battlebgsDir.exists() && battlebgsDir.listFiles()?.isNotEmpty() == true - return assetsExist && statsExist + println("Checking sprite files exist:") + println(" battle_sprites exists: $battleSpritesExist") + println(" extracted_assets exists: $assetsExist") + println(" extracted_digimon_stats exists: $statsExist") + println(" extracted_atksprites exists: $atkspritesExist") + println(" extracted_battlebgs exists: $battlebgsExist") + + return battleSpritesExist && assetsExist && statsExist && atkspritesExist && battlebgsExist } fun clearSpriteFiles() { try { - val extractedAssetsDir = File(context.filesDir, "battle_sprites/extracted_assets") - val extractedStatsDir = File(context.filesDir, "battle_sprites/extracted_digimon_stats") - - if (extractedAssetsDir.exists()) { - deleteDirectory(extractedAssetsDir) - println("Cleared extracted_assets directory") - } - - if (extractedStatsDir.exists()) { - deleteDirectory(extractedStatsDir) - println("Cleared extracted_digimon_stats directory") - } - - // Also clear the battle_sprites directory if it's empty val battleSpritesDir = File(context.filesDir, "battle_sprites") - if (battleSpritesDir.exists() && battleSpritesDir.listFiles()?.isEmpty() == true) { - battleSpritesDir.delete() + + if (battleSpritesDir.exists()) { + deleteDirectory(battleSpritesDir) println("Cleared battle_sprites directory") } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt index 7956511..f7d4430 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt @@ -6,34 +6,33 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import com.github.nacabaro.vbhelper.battle.BattleSpriteManager @Composable fun SpriteImage( - spriteName: String, - atlasName: String, + characterId: String, + frameNumber: Int, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit ) { val context = LocalContext.current - val spriteManager = remember { BattleSpriteManager(context) } + val spriteManager = remember { IndividualSpriteManager(context) } var bitmap by remember { mutableStateOf(null) } - LaunchedEffect(spriteName, atlasName) { - println("Loading sprite: $spriteName from atlas: $atlasName") - bitmap = spriteManager.loadSprite(spriteName, atlasName) + LaunchedEffect(characterId, frameNumber) { + println("Loading sprite frame: $frameNumber for character: $characterId") + bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber) if (bitmap == null) { - println("Failed to load sprite: $spriteName from atlas: $atlasName") + println("Failed to load sprite frame: $frameNumber for character: $characterId") } else { - println("Successfully loaded sprite: $spriteName from atlas: $atlasName") + println("Successfully loaded sprite frame: $frameNumber for character: $characterId") } } bitmap?.let { bmp -> Image( bitmap = bmp.asImageBitmap(), - contentDescription = "Sprite: $spriteName", + contentDescription = "Sprite: $characterId frame $frameNumber", modifier = modifier, contentScale = contentScale ) 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 10afc2f..4582e3e 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 @@ -45,7 +45,6 @@ import com.github.nacabaro.vbhelper.battle.APIBattleCharacter import android.util.Log import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.battle.RetrofitHelper -import com.github.nacabaro.vbhelper.battle.SpriteImage import com.github.nacabaro.vbhelper.battle.AttackSpriteImage import com.github.nacabaro.vbhelper.battle.SpriteFileManager import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem @@ -286,7 +285,8 @@ fun PlayerBattleView( modifier = Modifier .size(80.dp) .scale(-1f, 1f), // Flip player Digimon horizontally - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, + reloadMappings = false ) // Attack sprite visibility and positioning based on attack phase @@ -500,7 +500,8 @@ fun OpponentBattleView( characterId = activeCharacter?.charaId ?: "dim011_mon01", animationType = animationType, modifier = Modifier.size(80.dp), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, + reloadMappings = false ) // Attack sprite visibility and positioning based on attack phase @@ -642,12 +643,13 @@ fun BattlesScreen() { // Initialize sprite files on first load LaunchedEffect(Unit) { + println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") val spriteFileManager = SpriteFileManager(context) if (!spriteFileManager.checkSpriteFilesExist()) { - println("Copying sprite files to internal storage...") + println("BATTLESCREEN: Copying sprite files to internal storage...") spriteFileManager.copySpriteFilesToInternalStorage() } else { - println("Sprite files already exist in internal storage") + println("BATTLESCREEN: Sprite files already exist in internal storage") } } @@ -856,7 +858,8 @@ fun BattlesScreen() { characterId = testCharacterId, animationType = currentTestAnimation, modifier = Modifier.size(120.dp), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, + reloadMappings = false ) Spacer(modifier = Modifier.height(16.dp)) From c404f4f436eb35d4d9b63355dc42c8a1b355c3aa Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 07:01:39 -0400 Subject: [PATCH 38/89] Raised opponent HP bar. --- .../vbhelper/screens/BattlesScreen.kt | 192 +++++++++--------- 1 file changed, 100 insertions(+), 92 deletions(-) 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 4582e3e..c41ec2b 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 @@ -473,111 +473,119 @@ fun OpponentBattleView( var previousAttackPhase by remember { mutableStateOf(null) } var isTransitioning by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + Box( + modifier = Modifier.fillMaxSize() ) { - // Opponent Digimon - Box( + // Top section: Enemy HP bar and HP numbers + Column( modifier = Modifier .fillMaxWidth() - .size(80.dp), - contentAlignment = Alignment.CenterEnd + .padding(16.dp) ) { - // Determine animation type based on battle state - val animationType = when (battleSystem.attackPhase) { - 1 -> DigimonAnimationType.IDLE // Player attack on player screen - 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen - 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen - 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen - else -> DigimonAnimationType.IDLE - } - - AnimatedSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - animationType = animationType, - modifier = Modifier.size(80.dp), - contentScale = ContentScale.Fit, - reloadMappings = false + // Enemy HP bar + LinearProgressIndicator( + progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray ) - - // Attack sprite visibility and positioning based on attack phase - val shouldShowAttack = when (battleSystem.attackPhase) { - 1 -> false // Player attack on player screen - 2 -> true // Player attack on opponent screen - 3 -> true // Opponent attack on opponent screen - 4 -> false // Opponent attack on player screen - else -> false - } - - if (shouldShowAttack) { - val xOffset = when (battleSystem.attackPhase) { - 2 -> (attackAnimationProgress * 400 - 350).dp // Player attack on opponent screen - start more to the left - 3 -> (-attackAnimationProgress * 400 + -50).dp // Opponent attack on opponent screen - start more to the left - else -> 0.dp + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + } + + // Middle section: Opponent Digimon + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + // Opponent Digimon + Box( + modifier = Modifier + .fillMaxWidth() + .size(80.dp), + contentAlignment = Alignment.CenterEnd + ) { + // Determine animation type based on battle state + val animationType = when (battleSystem.attackPhase) { + 1 -> DigimonAnimationType.IDLE // Player attack on player screen + 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen + 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen + 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen + else -> DigimonAnimationType.IDLE } - // Use correct character ID based on attack phase - val characterId = when (battleSystem.attackPhase) { - 2 -> playerCharacter?.charaId ?: "dim011_mon01" // Use player's character ID for player attack - 3 -> activeCharacter?.charaId ?: "dim011_mon01" // Use opponent's character ID for opponent attack - else -> "dim011_mon01" + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = animationType, + modifier = Modifier.size(80.dp), + contentScale = ContentScale.Fit, + reloadMappings = false + ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { + 1 -> false // Player attack on player screen + 2 -> true // Player attack on opponent screen + 3 -> true // Opponent attack on opponent screen + 4 -> false // Opponent attack on player screen + else -> false } - // Handle sprite transition - LaunchedEffect(characterId, battleSystem.attackPhase) { - if ((previousCharacterId != null && previousCharacterId != characterId) || - (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { - // Character ID or attack phase changed, start transition - isTransitioning = true - delay(100) // Brief invisibility period - isTransitioning = false + if (shouldShowAttack) { + val xOffset = when (battleSystem.attackPhase) { + 2 -> (attackAnimationProgress * 400 - 350).dp // Player attack on opponent screen - start more to the left + 3 -> (-attackAnimationProgress * 400 + -50).dp // Opponent attack on opponent screen - start more to the left + else -> 0.dp + } + + // Use correct character ID based on attack phase + val characterId = when (battleSystem.attackPhase) { + 2 -> playerCharacter?.charaId ?: "dim011_mon01" // Use player's character ID for player attack + 3 -> activeCharacter?.charaId ?: "dim011_mon01" // Use opponent's character ID for opponent attack + else -> "dim011_mon01" + } + + // Handle sprite transition + LaunchedEffect(characterId, battleSystem.attackPhase) { + if ((previousCharacterId != null && previousCharacterId != characterId) || + (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { + // Character ID or attack phase changed, start transition + isTransitioning = true + delay(100) // Brief invisibility period + isTransitioning = false + } + previousCharacterId = characterId + previousAttackPhase = battleSystem.attackPhase + } + + println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") + + if (!isTransitioning) { + AttackSpriteImage( + characterId = characterId, + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only + contentScale = ContentScale.Fit + ) } - previousCharacterId = characterId - previousAttackPhase = battleSystem.attackPhase - } - - println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - - if (!isTransitioning) { - AttackSpriteImage( - characterId = characterId, - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) - .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only - contentScale = ContentScale.Fit - ) } } } - - // Enemy HP bar - LinearProgressIndicator( - progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), - color = Color.Red, - trackColor = Color.Gray - ) - - // Enemy HP display numbers - Text( - text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black - ) - - // Spacer for layout balance - Spacer(modifier = Modifier.height(120.dp)) } } From f0f1d9830e6381803b2f9276f0fd072a5923b9cf Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 16:14:17 -0400 Subject: [PATCH 39/89] Added dodge and attack hit animations. --- .../vbhelper/battle/ArenaBattleSystem.kt | 187 ++++++++++ .../vbhelper/screens/BattlesScreen.kt | 326 ++++++++++++++++-- 2 files changed, 475 insertions(+), 38 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 09999f7..3c4d048 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 @@ -43,6 +43,46 @@ class ArenaBattleSystem { private var _critBarProgress by mutableStateOf(0) val critBarProgress: Int get() = _critBarProgress + // Dodge animation states + private var _isDodging by mutableStateOf(false) + val isDodging: Boolean get() = _isDodging + + private var _dodgeProgress by mutableStateOf(0f) + val dodgeProgress: Float get() = _dodgeProgress + + private var _dodgeDirection by mutableStateOf(1f) // 1f = up, -1f = down + val dodgeDirection: Float get() = _dodgeDirection + + private var _isHit by mutableStateOf(false) + val isHit: Boolean get() = _isHit + + private var _hitProgress by mutableStateOf(0f) + val hitProgress: Float get() = _hitProgress + + // Separate states for player and opponent + private var _isPlayerDodging by mutableStateOf(false) + val isPlayerDodging: Boolean get() = _isPlayerDodging + + private var _isOpponentDodging by mutableStateOf(false) + val isOpponentDodging: Boolean get() = _isOpponentDodging + + private var _isPlayerHit by mutableStateOf(false) + val isPlayerHit: Boolean get() = _isPlayerHit + + private var _isOpponentHit by mutableStateOf(false) + val isOpponentHit: Boolean get() = _isOpponentHit + + // Counter-attack tracking + private var _shouldCounterAttack by mutableStateOf(false) + val shouldCounterAttack: Boolean get() = _shouldCounterAttack + + private var _counterAttackIsHit by mutableStateOf(false) + val counterAttackIsHit: Boolean get() = _counterAttackIsHit + + // Separate tracking for opponent attack result + private var _opponentAttackIsHit by mutableStateOf(false) + val opponentAttackIsHit: Boolean get() = _opponentAttackIsHit + fun startPlayerAttack() { Log.d(TAG, "Starting player attack") _attackPhase = 1 @@ -121,6 +161,18 @@ class ArenaBattleSystem { _isPlayerAttacking = false _attackIsHit = false _currentView = 0 + _isDodging = false + _dodgeProgress = 0f + _dodgeDirection = 1f + _isHit = false + _hitProgress = 0f + _isPlayerDodging = false + _isOpponentDodging = false + _isPlayerHit = false + _isOpponentHit = false + _shouldCounterAttack = false + _counterAttackIsHit = false + _opponentAttackIsHit = false Log.d(TAG, "Reset attack state") } @@ -137,4 +189,139 @@ class ArenaBattleSystem { _critBarProgress = progress //Log.d(TAG, "Updated crit bar progress: $progress") } + + // Dodge animation methods + fun startDodge() { + _isDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f // Start moving up + Log.d(TAG, "Started dodge animation") + } + + fun setDodgeProgress(progress: Float) { + _dodgeProgress = progress + } + + fun setDodgeDirection(direction: Float) { + _dodgeDirection = direction + } + + fun endDodge() { + _isDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended dodge animation") + } + + // Hit animation methods + fun startHit() { + _isHit = true + _hitProgress = 0f + Log.d(TAG, "Started hit animation") + } + + fun setHitProgress(progress: Float) { + _hitProgress = progress + } + + fun endHit() { + _isHit = false + _hitProgress = 0f + Log.d(TAG, "Ended hit animation") + } + + // Player-specific dodge methods + fun startPlayerDodge() { + _isPlayerDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f + Log.d(TAG, "Started player dodge animation") + } + + fun endPlayerDodge() { + _isPlayerDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended player dodge animation") + } + + // Opponent-specific dodge methods + fun startOpponentDodge() { + _isOpponentDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f + Log.d(TAG, "Started opponent dodge animation") + } + + fun endOpponentDodge() { + _isOpponentDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended opponent dodge animation") + } + + // Player-specific hit methods + fun startPlayerHit() { + _isPlayerHit = true + _hitProgress = 0f + Log.d(TAG, "Started player hit animation") + } + + fun endPlayerHit() { + _isPlayerHit = false + _hitProgress = 0f + Log.d(TAG, "Ended player hit animation") + } + + // Opponent-specific hit methods + fun startOpponentHit() { + _isOpponentHit = true + _hitProgress = 0f + Log.d(TAG, "Started opponent hit animation") + } + + fun endOpponentHit() { + _isOpponentHit = false + _hitProgress = 0f + Log.d(TAG, "Ended opponent hit animation") + } + + // Combined method to handle attack result + fun handleAttackResult(isHit: Boolean) { + _attackIsHit = isHit + if (isHit) { + // Player attack hit - opponent gets hit + startOpponentHit() + } else { + // Player attack missed - opponent dodges + startOpponentDodge() + } + Log.d(TAG, "Handled player attack result: ${if (isHit) "HIT" else "DODGE"}") + } + + // Method to handle opponent attack result + fun handleOpponentAttackResult(isHit: Boolean) { + _opponentAttackIsHit = isHit + if (isHit) { + // Opponent attack hit - player gets hit + startPlayerHit() + } else { + // Opponent attack missed - player dodges + startPlayerDodge() + } + Log.d(TAG, "Handled opponent attack result: ${if (isHit) "HIT" else "DODGE"}") + } + + // Counter-attack methods + fun setupCounterAttack(isHit: Boolean) { + _shouldCounterAttack = true + _counterAttackIsHit = isHit + Log.d(TAG, "Setup counter-attack: ${if (isHit) "HIT" else "DODGE"}, isHit=$isHit") + } + + fun startCounterAttack() { + _attackPhase = 3 + _attackProgress = 0f + _isPlayerAttacking = false + _currentView = 1 + _opponentAttackIsHit = _counterAttackIsHit + Log.d(TAG, "Started counter-attack with opponentAttackIsHit=$_opponentAttackIsHit, counterAttackIsHit=$_counterAttackIsHit") + } } \ No newline at end of file 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 c41ec2b..03a9b61 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 @@ -37,6 +37,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.material3.ButtonDefaults @@ -52,6 +54,8 @@ import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import kotlin.math.sin +import kotlin.math.PI @Composable @@ -65,6 +69,7 @@ fun BattleScreen( context: android.content.Context? = null ) { val battleSystem = remember { ArenaBattleSystem() } + val coroutineScope = rememberCoroutineScope() // Initialize HP when battle starts LaunchedEffect(activeCharacter, opponentCharacter) { @@ -110,14 +115,36 @@ fun BattleScreen( while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) + + // Trigger animation when attack reaches the opponent (around 55% progress for opponent dodge) + if (progress >= 0.55f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { + if (battleSystem.attackIsHit) { + // Player attack hits opponent + println("Player attack hits opponent at progress $progress") + battleSystem.startOpponentHit() + } else { + // Player attack misses, opponent dodges + println("Player attack misses, opponent dodges at progress $progress") + battleSystem.startOpponentDodge() + } + } + delay(16) // 60 FPS } - println("Phase 2 completed, applying damage and starting opponent attack") - // Apply player's damage and start opponent attack - battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) - pendingOpponentDamage = 0f - delay(500) - battleSystem.startOpponentAttack() + println("Phase 2 completed, applying damage and starting opponent attack") + // Apply player's damage + battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) + pendingOpponentDamage = 0f + delay(500) + + // Check if there's a counter-attack set up + if (battleSystem.shouldCounterAttack) { + println("Starting counter-attack") + battleSystem.startCounterAttack() + } else { + // Normal opponent attack + battleSystem.startOpponentAttack() + } } 3 -> { // Phase 3: Opponent attack on opponent screen @@ -140,14 +167,31 @@ fun BattleScreen( while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) + + // Trigger animation when attack reaches the player (around 75% progress - earlier for better timing) + if (progress >= 0.75f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { + println("Phase 4: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, shouldCounterAttack=${battleSystem.shouldCounterAttack}, counterAttackIsHit=${battleSystem.counterAttackIsHit}") + println("Phase 4: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") + println("Phase 4: Full debug - attackPhase=${battleSystem.attackPhase}, isPlayerAttacking=${battleSystem.isPlayerAttacking}") + if (battleSystem.opponentAttackIsHit) { + // Opponent attack hits player + println("Opponent attack hits player at progress $progress") + battleSystem.startPlayerHit() + } else { + // Opponent attack misses, player dodges + println("Opponent attack misses, player dodges at progress $progress") + battleSystem.startPlayerDodge() + } + } + delay(16) // 60 FPS } - println("Phase 4 completed, applying damage and resetting") - // Apply opponent's damage and reset - battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) - pendingPlayerDamage = 0f - battleSystem.resetAttackState() - battleSystem.enableAttackButton() + println("Phase 4 completed, applying damage and resetting") + // Apply opponent's damage and reset + battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + pendingPlayerDamage = 0f + battleSystem.resetAttackState() + battleSystem.enableAttackButton() // Check if battle is over if (battleSystem.checkBattleOver()) { @@ -158,6 +202,112 @@ fun BattleScreen( } } + // Player dodge animation + LaunchedEffect(battleSystem.isPlayerDodging) { + if (battleSystem.isPlayerDodging) { + println("Starting player dodge animation") + var dodgeProgress = 0f + var dodgeDirection = 1f // Start moving up + + // Move up + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f // Faster dodge movement + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) // 60 FPS + } + + // Wait at the top + delay(200) + + // Move back down + dodgeDirection = -1f + dodgeProgress = 0f + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) + } + + battleSystem.endPlayerDodge() + println("Player dodge animation completed") + } + } + + // Opponent dodge animation + LaunchedEffect(battleSystem.isOpponentDodging) { + if (battleSystem.isOpponentDodging) { + println("Starting opponent dodge animation") + var dodgeProgress = 0f + var dodgeDirection = 1f // Start moving up + + // Move up + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f // Faster dodge movement + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) // 60 FPS + } + + // Wait at the top + delay(200) + + // Move back down + dodgeDirection = -1f + dodgeProgress = 0f + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) + } + + battleSystem.endOpponentDodge() + println("Opponent dodge animation completed") + } + } + + // Player hit animation + LaunchedEffect(battleSystem.isPlayerHit) { + if (battleSystem.isPlayerHit) { + println("Starting player hit animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endPlayerHit() + println("Player hit animation completed") + } + } + + // Opponent hit animation + LaunchedEffect(battleSystem.isOpponentHit) { + if (battleSystem.isOpponentHit) { + println("Starting opponent hit animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endOpponentHit() + println("Opponent hit animation completed") + } + } + Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -179,7 +329,8 @@ fun BattleScreen( onSetPendingDamage = { playerDamage, opponentDamage -> pendingPlayerDamage = playerDamage pendingOpponentDamage = opponentDamage - } + }, + coroutineScope = coroutineScope ) } 1 -> { @@ -207,12 +358,14 @@ fun PlayerBattleView( activeCharacter: APIBattleCharacter?, context: android.content.Context?, opponent: APIBattleCharacter?, - onSetPendingDamage: (Float, Float) -> Unit + onSetPendingDamage: (Float, Float) -> Unit, + coroutineScope: kotlinx.coroutines.CoroutineScope ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } var previousAttackPhase by remember { mutableStateOf(null) } var isTransitioning by remember { mutableStateOf(false) } + var lastApiResult by remember { mutableStateOf(null) } Box( modifier = Modifier.fillMaxSize() @@ -238,6 +391,16 @@ fun PlayerBattleView( Spacer(modifier = Modifier.height(16.dp)) + // Debug display + if (lastApiResult != null) { + Text( + text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", + color = Color.Red, + fontSize = 10.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + } + // Health bar LinearProgressIndicator( progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), @@ -271,20 +434,54 @@ fun PlayerBattleView( contentAlignment = Alignment.CenterStart ) { // Determine animation type based on battle state - val animationType = when (battleSystem.attackPhase) { - 1 -> DigimonAnimationType.ATTACK // Player attack on player screen - 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen - 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen - 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen + val animationType = when { + battleSystem.isPlayerDodging -> DigimonAnimationType.WALK // Use walk animation for dodge + battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Player attack on player screen + battleSystem.attackPhase == 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen + battleSystem.attackPhase == 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen + battleSystem.attackPhase == 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen else -> DigimonAnimationType.IDLE } + // Calculate vertical offset for dodge animation + val verticalOffset = if (battleSystem.isPlayerDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.dodgeProgress + val direction = battleSystem.dodgeDirection + + if (direction > 0) { + // Moving up + (progress * dodgeHeight.value).dp + } else { + // Moving back down + ((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect (slight shake) + val hitOffset = if (battleSystem.isPlayerHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + // Simple shake effect without complex math + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + AnimatedSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", animationType = animationType, modifier = Modifier .size(80.dp) - .scale(-1f, 1f), // Flip player Digimon horizontally + .scale(-1f, 1f) // Flip player Digimon horizontally + .offset( + x = hitOffset, + y = verticalOffset + ), contentScale = ContentScale.Fit, reloadMappings = false ) @@ -407,6 +604,7 @@ fun PlayerBattleView( ) { apiResult -> // Handle API response here println("API Result: $apiResult") + lastApiResult = apiResult // Store for debug display // Update HP based on API response when (apiResult.state) { @@ -414,18 +612,35 @@ fun PlayerBattleView( // Match is still ongoing - update HP and continue println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Set pending damage based on API result - if (apiResult.playerAttackHit) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) - } else { - // Player attack missed - enemy counter-attacks and player takes damage - println("Player attack missed! Enemy counter-attacks and player takes ${apiResult.opponentAttackDamage} damage") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage - battleSystem.setAttackHitState(false) - } + // Set pending damage based on API result + if (apiResult.playerAttackDamage > 0) { + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - enemy counter-attacks + println("Player attack missed! Enemy counter-attacks") + battleSystem.setAttackHitState(false) + // Set up counter-attack - determine if it hits based on API result + val counterAttackHits = apiResult.opponentAttackDamage > 0 + println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") + println("Full API response: status=${apiResult.status}, state=${apiResult.state}, playerAttackHit=${apiResult.playerAttackHit}, playerAttackDamage=${apiResult.playerAttackDamage}, opponentAttackDamage=${apiResult.opponentAttackDamage}, playerHP=${apiResult.playerHP}, opponentHP=${apiResult.opponentHP}") + println("DEBUG: Using playerAttackDamage > 0 instead of playerAttackHit for hit detection") + + // Use opponentAttackDamage to determine counter-attack hit + val finalCounterAttackHits = counterAttackHits + println("Using opponentAttackDamage > 0 for counter-attack: $finalCounterAttackHits") + + if (finalCounterAttackHits) { + println("Counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + } else { + println("Counter-attack misses! Player dodges") + onSetPendingDamage(0f, 0f) // No damage + } + battleSystem.setupCounterAttack(finalCounterAttackHits) + } } 2 -> { // Match is over - transition to results screen @@ -515,18 +730,53 @@ fun OpponentBattleView( contentAlignment = Alignment.CenterEnd ) { // Determine animation type based on battle state - val animationType = when (battleSystem.attackPhase) { - 1 -> DigimonAnimationType.IDLE // Player attack on player screen - 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen - 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen - 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen + val animationType = when { + battleSystem.isOpponentDodging -> DigimonAnimationType.WALK // Use walk animation for dodge + battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.attackPhase == 1 -> DigimonAnimationType.IDLE // Player attack on player screen + battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen + battleSystem.attackPhase == 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen + battleSystem.attackPhase == 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen else -> DigimonAnimationType.IDLE } + // Calculate vertical offset for dodge animation + val verticalOffset = if (battleSystem.isOpponentDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.dodgeProgress + val direction = battleSystem.dodgeDirection + + if (direction > 0) { + // Moving up + (progress * dodgeHeight.value).dp + } else { + // Moving back down + ((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect (slight shake) + val hitOffset = if (battleSystem.isOpponentHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + // Simple shake effect without complex math + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + AnimatedSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", animationType = animationType, - modifier = Modifier.size(80.dp), + modifier = Modifier + .size(80.dp) + .offset( + x = hitOffset, + y = verticalOffset + ), contentScale = ContentScale.Fit, reloadMappings = false ) From 7843be700441d06b0fa33c283667955bfe739faf Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 17:35:44 -0400 Subject: [PATCH 40/89] Fixed dodge animation. Sprites now move up then back to original location. --- .../vbhelper/battle/ArenaBattleSystem.kt | 45 ++++++++++++++++--- .../vbhelper/screens/BattlesScreen.kt | 40 ++++++++--------- 2 files changed, 59 insertions(+), 26 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 3c4d048..6105bd3 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 @@ -66,6 +66,19 @@ class ArenaBattleSystem { private var _isOpponentDodging by mutableStateOf(false) val isOpponentDodging: Boolean get() = _isOpponentDodging + // Separate dodge progress and direction for player and opponent + private var _playerDodgeProgress by mutableStateOf(0f) + val playerDodgeProgress: Float get() = _playerDodgeProgress + + private var _playerDodgeDirection by mutableStateOf(1f) + val playerDodgeDirection: Float get() = _playerDodgeDirection + + private var _opponentDodgeProgress by mutableStateOf(0f) + val opponentDodgeProgress: Float get() = _opponentDodgeProgress + + private var _opponentDodgeDirection by mutableStateOf(1f) + val opponentDodgeDirection: Float get() = _opponentDodgeDirection + private var _isPlayerHit by mutableStateOf(false) val isPlayerHit: Boolean get() = _isPlayerHit @@ -168,6 +181,10 @@ class ArenaBattleSystem { _hitProgress = 0f _isPlayerDodging = false _isOpponentDodging = false + _playerDodgeProgress = 0f + _playerDodgeDirection = 1f + _opponentDodgeProgress = 0f + _opponentDodgeDirection = 1f _isPlayerHit = false _isOpponentHit = false _shouldCounterAttack = false @@ -232,31 +249,47 @@ class ArenaBattleSystem { // Player-specific dodge methods fun startPlayerDodge() { _isPlayerDodging = true - _dodgeProgress = 0f - _dodgeDirection = 1f + _playerDodgeProgress = 0f + _playerDodgeDirection = 1f Log.d(TAG, "Started player dodge animation") } fun endPlayerDodge() { _isPlayerDodging = false - _dodgeProgress = 0f + _playerDodgeProgress = 0f Log.d(TAG, "Ended player dodge animation") } + fun setPlayerDodgeProgress(progress: Float) { + _playerDodgeProgress = progress + } + + fun setPlayerDodgeDirection(direction: Float) { + _playerDodgeDirection = direction + } + // Opponent-specific dodge methods fun startOpponentDodge() { _isOpponentDodging = true - _dodgeProgress = 0f - _dodgeDirection = 1f + _opponentDodgeProgress = 0f + _opponentDodgeDirection = 1f Log.d(TAG, "Started opponent dodge animation") } fun endOpponentDodge() { _isOpponentDodging = false - _dodgeProgress = 0f + _opponentDodgeProgress = 0f Log.d(TAG, "Ended opponent dodge animation") } + fun setOpponentDodgeProgress(progress: Float) { + _opponentDodgeProgress = progress + } + + fun setOpponentDodgeDirection(direction: Float) { + _opponentDodgeDirection = direction + } + // Player-specific hit methods fun startPlayerHit() { _isPlayerHit = true 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 03a9b61..0311542 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 @@ -212,8 +212,8 @@ fun BattleScreen( // Move up while (dodgeProgress < 1f) { dodgeProgress += 0.05f // Faster dodge movement - battleSystem.setDodgeProgress(dodgeProgress) - battleSystem.setDodgeDirection(dodgeDirection) + battleSystem.setPlayerDodgeProgress(dodgeProgress) + battleSystem.setPlayerDodgeDirection(dodgeDirection) delay(16) // 60 FPS } @@ -225,8 +225,8 @@ fun BattleScreen( dodgeProgress = 0f while (dodgeProgress < 1f) { dodgeProgress += 0.05f - battleSystem.setDodgeProgress(dodgeProgress) - battleSystem.setDodgeDirection(dodgeDirection) + battleSystem.setPlayerDodgeProgress(dodgeProgress) + battleSystem.setPlayerDodgeDirection(dodgeDirection) delay(16) } @@ -245,8 +245,8 @@ fun BattleScreen( // Move up while (dodgeProgress < 1f) { dodgeProgress += 0.05f // Faster dodge movement - battleSystem.setDodgeProgress(dodgeProgress) - battleSystem.setDodgeDirection(dodgeDirection) + battleSystem.setOpponentDodgeProgress(dodgeProgress) + battleSystem.setOpponentDodgeDirection(dodgeDirection) delay(16) // 60 FPS } @@ -258,8 +258,8 @@ fun BattleScreen( dodgeProgress = 0f while (dodgeProgress < 1f) { dodgeProgress += 0.05f - battleSystem.setDodgeProgress(dodgeProgress) - battleSystem.setDodgeDirection(dodgeDirection) + battleSystem.setOpponentDodgeProgress(dodgeProgress) + battleSystem.setOpponentDodgeDirection(dodgeDirection) delay(16) } @@ -447,15 +447,15 @@ fun PlayerBattleView( // Calculate vertical offset for dodge animation val verticalOffset = if (battleSystem.isPlayerDodging) { val dodgeHeight = 30.dp - val progress = battleSystem.dodgeProgress - val direction = battleSystem.dodgeDirection + val progress = battleSystem.playerDodgeProgress + val direction = battleSystem.playerDodgeDirection if (direction > 0) { - // Moving up - (progress * dodgeHeight.value).dp + // Moving up (negative offset to move UP visually) + -(progress * dodgeHeight.value).dp } else { - // Moving back down - ((1f - progress) * dodgeHeight.value).dp + // Moving back down (from negative peak to 0) + -((1f - progress) * dodgeHeight.value).dp } } else { 0.dp @@ -743,15 +743,15 @@ fun OpponentBattleView( // Calculate vertical offset for dodge animation val verticalOffset = if (battleSystem.isOpponentDodging) { val dodgeHeight = 30.dp - val progress = battleSystem.dodgeProgress - val direction = battleSystem.dodgeDirection + val progress = battleSystem.opponentDodgeProgress + val direction = battleSystem.opponentDodgeDirection if (direction > 0) { - // Moving up - (progress * dodgeHeight.value).dp + // Moving up (negative offset to move UP visually) + -(progress * dodgeHeight.value).dp } else { - // Moving back down - ((1f - progress) * dodgeHeight.value).dp + // Moving back down (from negative peak to 0) + -((1f - progress) * dodgeHeight.value).dp } } else { 0.dp From b3cf823c3f4edb75e1b8b4f1fcf51a712b53ab5b Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 18:35:50 -0400 Subject: [PATCH 41/89] Swapped to 3 panel battle. --- .../vbhelper/screens/BattlesScreen.kt | 545 +++++++++++++++--- 1 file changed, 463 insertions(+), 82 deletions(-) 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 0311542..f16f13d 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 @@ -96,8 +96,8 @@ fun BattleScreen( LaunchedEffect(battleSystem.attackPhase) { when (battleSystem.attackPhase) { 1 -> { - // Phase 1: Player attack on player screen - println("Starting Phase 1: Player attack on player screen") + // Phase 1: Both attacks from middle screen + println("Starting Phase 1: Both attacks from middle screen") var progress = 0f while (progress < 1f) { progress += 0.016f // 60 FPS @@ -108,86 +108,72 @@ fun BattleScreen( battleSystem.advanceAttackPhase() } 2 -> { - // Phase 2: Player attack on opponent screen - println("Starting Phase 2: Player attack on opponent screen") - battleSystem.switchToView(1) + // Phase 2: Player attack on enemy screen + println("Starting Phase 2: Player attack on enemy screen") + battleSystem.switchToView(2) // Enemy screen var progress = 0f while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the opponent (around 55% progress for opponent dodge) + // Trigger animation when attack reaches the enemy (around 55% progress for enemy dodge) if (progress >= 0.55f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { if (battleSystem.attackIsHit) { - // Player attack hits opponent - println("Player attack hits opponent at progress $progress") + // Player attack hits enemy + println("Player attack hits enemy at progress $progress") battleSystem.startOpponentHit() } else { - // Player attack misses, opponent dodges - println("Player attack misses, opponent dodges at progress $progress") + // Player attack misses, enemy dodges + println("Player attack misses, enemy dodges at progress $progress") battleSystem.startOpponentDodge() } } delay(16) // 60 FPS } - println("Phase 2 completed, applying damage and starting opponent attack") + println("Phase 2 completed, applying damage and starting Phase 3") // Apply player's damage battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) pendingOpponentDamage = 0f delay(500) - // Check if there's a counter-attack set up + // Check if there should be a counter-attack if (battleSystem.shouldCounterAttack) { - println("Starting counter-attack") + println("Starting counter-attack from Phase 2") battleSystem.startCounterAttack() } else { - // Normal opponent attack - battleSystem.startOpponentAttack() + println("No counter-attack, advancing to Phase 3") + battleSystem.advanceAttackPhase() } } 3 -> { - // Phase 3: Opponent attack on opponent screen - println("Starting Phase 3: Opponent attack on opponent screen") - battleSystem.switchToView(1) - var progress = 0f - while (progress < 1f) { - progress += 0.016f // 60 FPS - battleSystem.setAttackProgress(progress) - delay(16) // 60 FPS - } - println("Phase 3 completed, advancing to Phase 4") - battleSystem.advanceAttackPhase() - } - 4 -> { - // Phase 4: Opponent attack on player screen - println("Starting Phase 4: Opponent attack on player screen") - battleSystem.switchToView(0) + // Phase 3: Enemy attack on player screen + println("Starting Phase 3: Enemy attack on player screen") + battleSystem.switchToView(1) // Player screen var progress = 0f while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the player (around 75% progress - earlier for better timing) + // Trigger animation when attack reaches the player (around 75% progress for player dodge) if (progress >= 0.75f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { - println("Phase 4: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, shouldCounterAttack=${battleSystem.shouldCounterAttack}, counterAttackIsHit=${battleSystem.counterAttackIsHit}") - println("Phase 4: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") - println("Phase 4: Full debug - attackPhase=${battleSystem.attackPhase}, isPlayerAttacking=${battleSystem.isPlayerAttacking}") + println("Phase 3: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}") + println("Phase 3: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") if (battleSystem.opponentAttackIsHit) { - // Opponent attack hits player - println("Opponent attack hits player at progress $progress") + // Enemy attack hits player + println("Enemy attack hits player at progress $progress") battleSystem.startPlayerHit() } else { - // Opponent attack misses, player dodges - println("Opponent attack misses, player dodges at progress $progress") + // Enemy attack misses, player dodges + println("Enemy attack misses, player dodges at progress $progress") battleSystem.startPlayerDodge() } } delay(16) // 60 FPS } - println("Phase 4 completed, applying damage and resetting") - // Apply opponent's damage and reset + println("Phase 3 completed, applying damage and resetting") + // Apply enemy's damage and reset battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) pendingPlayerDamage = 0f battleSystem.resetAttackState() @@ -314,7 +300,28 @@ fun BattleScreen( ) { when (battleSystem.currentView) { 0 -> { - // Player view + // Middle screen - both Digimon + MiddleBattleView( + battleSystem = battleSystem, + stage = stage, + playerName = playerName, + opponentName = opponentName, + attackAnimationProgress = battleSystem.attackProgress, + onAttackClick = { + battleSystem.startPlayerAttack() + }, + activeCharacter = activeCharacter, + opponentCharacter = opponentCharacter, + context = context, + onSetPendingDamage = { playerDamage, opponentDamage -> + pendingPlayerDamage = playerDamage + pendingOpponentDamage = opponentDamage + }, + coroutineScope = coroutineScope + ) + } + 1 -> { + // Player screen - enemy attack PlayerBattleView( battleSystem = battleSystem, stage = stage, @@ -333,9 +340,9 @@ fun BattleScreen( coroutineScope = coroutineScope ) } - 1 -> { - // Opponent view - OpponentBattleView( + 2 -> { + // Enemy screen - player attack + EnemyBattleView( battleSystem = battleSystem, stage = stage, opponentName = opponentName, @@ -348,6 +355,394 @@ fun BattleScreen( } } +@Composable +fun MiddleBattleView( + battleSystem: ArenaBattleSystem, + stage: String, + playerName: String, + opponentName: String, + attackAnimationProgress: Float, + onAttackClick: () -> Unit, + activeCharacter: APIBattleCharacter?, + opponentCharacter: APIBattleCharacter?, + context: android.content.Context?, + onSetPendingDamage: (Float, Float) -> Unit, + coroutineScope: kotlinx.coroutines.CoroutineScope +) { + // Track previous character ID to detect transitions + var previousCharacterId by remember { mutableStateOf(null) } + var previousAttackPhase by remember { mutableStateOf(null) } + var isTransitioning by remember { mutableStateOf(false) } + var lastApiResult by remember { mutableStateOf(null) } + + Box( + modifier = Modifier.fillMaxSize() + ) { + // Top section: Exit button, HP bars, and HP numbers + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // Exit button at the top-right + Box( + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = { /* TODO: Add exit functionality */ }, + modifier = Modifier.align(Alignment.TopEnd), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) + ) { + Text("Exit", color = Color.White, fontSize = 14.sp) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Debug display + if (lastApiResult != null) { + Text( + text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", + color = Color.Red, + fontSize = 10.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + // Enemy HP bar (top) + LinearProgressIndicator( + progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray + ) + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Player HP bar (bottom) + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Player HP display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + } + + // Middle section: Both Digimon with horizontal line separator + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly + ) { + // Enemy Digimon (top half) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.CenterEnd + ) { + // Determine animation type for enemy + val enemyAnimationType = when { + battleSystem.isOpponentDodging -> DigimonAnimationType.WALK + battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking + else -> DigimonAnimationType.IDLE + } + + // Calculate vertical offset for enemy dodge animation + val enemyVerticalOffset = if (battleSystem.isOpponentDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.opponentDodgeProgress + val direction = battleSystem.opponentDodgeDirection + + if (direction > 0) { + -(progress * dodgeHeight.value).dp + } else { + -((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect for enemy + val enemyHitOffset = if (battleSystem.isOpponentHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + + AnimatedSpriteImage( + characterId = opponentCharacter?.charaId ?: "dim011_mon01", + animationType = enemyAnimationType, + modifier = Modifier + .size(80.dp) + .offset( + x = enemyHitOffset, + y = enemyVerticalOffset + ), + contentScale = ContentScale.Fit, + reloadMappings = false + ) + + // Enemy attack sprite (Phase 1 only) + if (battleSystem.attackPhase == 1) { + val xOffset = (attackAnimationProgress * 400 + 50).dp // Move right off screen + + AttackSpriteImage( + characterId = opponentCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ), + contentScale = ContentScale.Fit + ) + } + } + + // Horizontal line separator + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background(Color.Black) + ) + + // Player Digimon (bottom half) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.CenterStart + ) { + // Determine animation type for player + val playerAnimationType = when { + battleSystem.isPlayerDodging -> DigimonAnimationType.WALK + battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking + else -> DigimonAnimationType.IDLE + } + + // Calculate vertical offset for player dodge animation + val playerVerticalOffset = if (battleSystem.isPlayerDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.playerDodgeProgress + val direction = battleSystem.playerDodgeDirection + + if (direction > 0) { + -(progress * dodgeHeight.value).dp + } else { + -((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect for player + val playerHitOffset = if (battleSystem.isPlayerHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = playerAnimationType, + modifier = Modifier + .size(80.dp) + .scale(-1f, 1f) // Flip player Digimon horizontally + .offset( + x = playerHitOffset, + y = playerVerticalOffset + ), + contentScale = ContentScale.Fit, + reloadMappings = false + ) + + // Player attack sprite (Phase 1 only) + if (battleSystem.attackPhase == 1) { + val xOffset = (-attackAnimationProgress * 400 - 50).dp // Move left off screen + + AttackSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) + .scale(-1f, 1f), // Flip attack sprite + contentScale = ContentScale.Fit + ) + } + } + } + } + + // Bottom section: Critical bar and Attack button + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .align(Alignment.BottomCenter), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Critical bar + LinearProgressIndicator( + progress = battleSystem.critBarProgress / 100f, + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Yellow, + trackColor = Color.Gray + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Attack button + Button( + onClick = { + println("Attack button clicked!") + + // Get crit bar progress as float (0.0f to 100.0f) + val critBarProgressFloat = battleSystem.critBarProgress.toFloat() + + // Determine player and opponent stages + val playerStage = when (activeCharacter?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + val opponentStage = when (opponentCharacter?.stage) { + 0 -> 0 // rookie + 1 -> 1 // champion + 2 -> 2 // ultimate + 3 -> 3 // mega + else -> 0 + } + + // Send API call with all parameters + context?.let { ctx -> + // Start both attacks simultaneously + battleSystem.startPlayerAttack() + + RetrofitHelper().getPVPWinner( + ctx, + 1, + 2, + activeCharacter?.name ?: "Player", + playerStage, + opponentStage, + opponentCharacter?.name ?: "Opponent", + opponentStage + ) { apiResult -> + // Handle API response here + println("API Result: $apiResult") + lastApiResult = apiResult // Store for debug display + + // Update HP based on API response + when (apiResult.state) { + 1 -> { + // Match is still ongoing - update HP and continue + println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") + + // Set pending damage based on API result + if (apiResult.playerAttackDamage > 0) { + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - enemy counter-attacks + println("Player attack missed! Enemy counter-attacks") + battleSystem.setAttackHitState(false) + // Set up counter-attack - determine if it hits based on API result + val counterAttackHits = apiResult.opponentAttackDamage > 0 + println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") + println("Full API response: status=${apiResult.status}, state=${apiResult.state}, playerAttackHit=${apiResult.playerAttackHit}, playerAttackDamage=${apiResult.playerAttackDamage}, opponentAttackDamage=${apiResult.opponentAttackDamage}, playerHP=${apiResult.playerHP}, opponentHP=${apiResult.opponentHP}") + println("DEBUG: Using playerAttackDamage > 0 instead of playerAttackHit for hit detection") + + // Use opponentAttackDamage to determine counter-attack hit + val finalCounterAttackHits = counterAttackHits + println("Using opponentAttackDamage > 0 for counter-attack: $finalCounterAttackHits") + + if (finalCounterAttackHits) { + println("Counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + } else { + println("Counter-attack misses! Player dodges") + onSetPendingDamage(0f, 0f) // No damage + } + battleSystem.setupCounterAttack(finalCounterAttackHits) + } + } + 2 -> { + // Match is over - transition to results screen + println("Match is over! Winner: ${apiResult.winner}") + battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) + onAttackClick() // This will transition to battle-results screen + } + -1 -> { + // Error occurred + println("API Error: ${apiResult.status}") + battleSystem.resetAttackState() + battleSystem.enableAttackButton() + } + } + } + } + }, + enabled = battleSystem.isAttackButtonEnabled, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Red, + disabledContainerColor = Color.Gray + ), + shape = RoundedCornerShape(8.dp) + ) { + Text("Attack", color = Color.White, fontSize = 18.sp) + } + } + } +} + @Composable fun PlayerBattleView( battleSystem: ArenaBattleSystem, @@ -488,24 +883,21 @@ fun PlayerBattleView( // Attack sprite visibility and positioning based on attack phase val shouldShowAttack = when (battleSystem.attackPhase) { - 1 -> true // Player attack on player screen - 2 -> true // Player attack on opponent screen - 3 -> false // Opponent attack on opponent screen - 4 -> true // Opponent attack on player screen + 1 -> false // Both attacks from middle screen + 2 -> false // Player attack on enemy screen + 3 -> true // Enemy attack on player screen else -> false } if (shouldShowAttack) { val xOffset = when (battleSystem.attackPhase) { - 1 -> (attackAnimationProgress * 400 + 50).dp // Player attack on player screen - start and end more to the right - 2 -> (attackAnimationProgress * 400 - 200).dp // Player attack on opponent screen - 4 -> (-attackAnimationProgress * 400 + 350).dp // Opponent attack on player screen - start more to the right + 3 -> (-attackAnimationProgress * 400 + 350).dp // Enemy attack on player screen - start more to the right else -> 0.dp } - // Use opponent character ID for Phase 4 (opponent attack) + // Use opponent character ID for Phase 3 (enemy attack) val characterId = when (battleSystem.attackPhase) { - 4 -> opponent?.charaId ?: "dim011_mon01" // Use opponent's character ID + 3 -> opponent?.charaId ?: "dim011_mon01" // Use opponent's character ID else -> activeCharacter?.charaId ?: "dim011_mon01" // Use player's character ID } @@ -534,7 +926,7 @@ fun PlayerBattleView( x = xOffset, y = 0.dp ) - .scale(if (battleSystem.attackPhase == 4) 1f else -1f, 1f), // Don't flip opponent attacks + .scale(if (battleSystem.attackPhase == 3) 1f else -1f, 1f), // Don't flip enemy attacks contentScale = ContentScale.Fit ) } @@ -675,7 +1067,7 @@ fun PlayerBattleView( } @Composable -fun OpponentBattleView( +fun EnemyBattleView( battleSystem: ArenaBattleSystem, stage: String, opponentName: String, @@ -715,14 +1107,14 @@ fun OpponentBattleView( ) } - // Middle section: Opponent Digimon + // Middle section: Enemy Digimon Box( modifier = Modifier .fillMaxSize() .padding(16.dp), contentAlignment = Alignment.Center ) { - // Opponent Digimon + // Enemy Digimon Box( modifier = Modifier .fillMaxWidth() @@ -733,10 +1125,7 @@ fun OpponentBattleView( val animationType = when { battleSystem.isOpponentDodging -> DigimonAnimationType.WALK // Use walk animation for dodge battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) - battleSystem.attackPhase == 1 -> DigimonAnimationType.IDLE // Player attack on player screen - battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen - battleSystem.attackPhase == 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen - battleSystem.attackPhase == 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen + battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on enemy screen else -> DigimonAnimationType.IDLE } @@ -783,26 +1172,15 @@ fun OpponentBattleView( // Attack sprite visibility and positioning based on attack phase val shouldShowAttack = when (battleSystem.attackPhase) { - 1 -> false // Player attack on player screen - 2 -> true // Player attack on opponent screen - 3 -> true // Opponent attack on opponent screen - 4 -> false // Opponent attack on player screen + 2 -> true // Player attack on enemy screen else -> false } if (shouldShowAttack) { - val xOffset = when (battleSystem.attackPhase) { - 2 -> (attackAnimationProgress * 400 - 350).dp // Player attack on opponent screen - start more to the left - 3 -> (-attackAnimationProgress * 400 + -50).dp // Opponent attack on opponent screen - start more to the left - else -> 0.dp - } + val xOffset = (attackAnimationProgress * 400 - 350).dp // Player attack on enemy screen - start more to the left - // Use correct character ID based on attack phase - val characterId = when (battleSystem.attackPhase) { - 2 -> playerCharacter?.charaId ?: "dim011_mon01" // Use player's character ID for player attack - 3 -> activeCharacter?.charaId ?: "dim011_mon01" // Use opponent's character ID for opponent attack - else -> "dim011_mon01" - } + // Use player's character ID for player attack + val characterId = playerCharacter?.charaId ?: "dim011_mon01" // Handle sprite transition LaunchedEffect(characterId, battleSystem.attackPhase) { @@ -817,7 +1195,7 @@ fun OpponentBattleView( previousAttackPhase = battleSystem.attackPhase } - println("OpponentBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") + println("EnemyBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") if (!isTransitioning) { AttackSpriteImage( @@ -829,7 +1207,7 @@ fun OpponentBattleView( x = xOffset, y = 0.dp ) - .scale(if (battleSystem.attackPhase == 2) -1f else 1f, 1f), // Flip player attacks only + .scale(-1f, 1f), // Flip player attacks contentScale = ContentScale.Fit ) } @@ -1201,9 +1579,12 @@ fun BattlesScreen() { Scaffold ( topBar = { - TopBanner( - text = "Online battles" - ) + // Only show TopBanner when not in battle mode + if (currentView != "battle-main" && currentView != "battle-results") { + TopBanner( + text = "Online battles" + ) + } } ) { contentPadding -> Column( From a3bebcf290ed3127e7548c1f8a38f21a8ad94b77 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 19:42:31 -0400 Subject: [PATCH 42/89] Adjusted animation timings. --- .../nacabaro/vbhelper/battle/DigimonAnimationState.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 77028d3..2ff49cd 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -64,8 +64,8 @@ class DigimonAnimationStateMachine( // Animation durations for each type private val animationDurations = mapOf( - DigimonAnimationType.IDLE to 500L, - DigimonAnimationType.IDLE2 to 500L, + DigimonAnimationType.IDLE to 750L, + DigimonAnimationType.IDLE2 to 750L, DigimonAnimationType.WALK to 200L, DigimonAnimationType.WALK2 to 200L, DigimonAnimationType.RUN to 150L, @@ -73,8 +73,8 @@ class DigimonAnimationStateMachine( DigimonAnimationType.WORKOUT to 300L, DigimonAnimationType.WORKOUT2 to 300L, DigimonAnimationType.HAPPY to 400L, - DigimonAnimationType.SLEEP to 1000L, - DigimonAnimationType.ATTACK to 300L, + DigimonAnimationType.SLEEP to 1500L, + DigimonAnimationType.ATTACK to 650L, DigimonAnimationType.FLEE to 150L ) From 481c0b6d9a1375a31dfb09b6417519d48bf6c659 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 19:42:41 -0400 Subject: [PATCH 43/89] Adjusted BattlesScreen UI. --- .../vbhelper/screens/BattlesScreen.kt | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) 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 f16f13d..5e5616e 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 @@ -400,6 +400,7 @@ fun MiddleBattleView( Spacer(modifier = Modifier.height(16.dp)) // Debug display + /* if (lastApiResult != null) { Text( text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", @@ -408,6 +409,7 @@ fun MiddleBattleView( ) Spacer(modifier = Modifier.height(8.dp)) } + */ // Enemy HP bar (top) LinearProgressIndicator( @@ -428,22 +430,14 @@ fun MiddleBattleView( Spacer(modifier = Modifier.height(16.dp)) - // Player HP bar (bottom) - LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), - color = Color.Green, - trackColor = Color.Gray - ) - // Player HP display numbers + /* Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", fontSize = 14.sp, color = Color.Black ) + */ } // Middle section: Both Digimon with horizontal line separator @@ -611,7 +605,7 @@ fun MiddleBattleView( } } - // Bottom section: Critical bar and Attack button + // Bottom section: Player HP bar, Critical bar and Attack button Column( modifier = Modifier .fillMaxWidth() @@ -629,7 +623,26 @@ fun MiddleBattleView( trackColor = Color.Gray ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(6.dp)) + + // Player HP bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + // Player HP display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 14.sp, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(6.dp)) // Attack button Button( @@ -780,13 +793,14 @@ fun PlayerBattleView( modifier = Modifier.align(Alignment.TopEnd), colors = ButtonDefaults.buttonColors(containerColor = Color.Red) ) { - Text("Exit", color = Color.White, fontSize = 14.sp) + Text("Exit", color = Color.White, fontSize = 12.sp) } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) // Debug display + /* if (lastApiResult != null) { Text( text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", @@ -795,6 +809,7 @@ fun PlayerBattleView( ) Spacer(modifier = Modifier.height(8.dp)) } + */ // Health bar LinearProgressIndicator( @@ -1298,6 +1313,7 @@ fun BattlesScreen() { opponentsList.clear() opponentsList.addAll(opponents.opponentsList) currentView = "rookie" + currentStage = "rookie" } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1322,6 +1338,7 @@ fun BattlesScreen() { opponentsList.clear() opponentsList.addAll(opponents.opponentsList) currentView = "champion" + currentStage = "champion" } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1346,6 +1363,7 @@ fun BattlesScreen() { opponentsList.clear() opponentsList.addAll(opponents.opponentsList) currentView = "ultimate" + currentStage = "ultimate" } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1370,6 +1388,7 @@ fun BattlesScreen() { opponentsList.clear() opponentsList.addAll(opponents.opponentsList) currentView = "mega" + currentStage = "mega" } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1394,6 +1413,15 @@ fun BattlesScreen() { } val characterDropdown = @Composable { currentStage: String -> + // Get the appropriate character list based on the passed currentStage parameter + val characterListForStage = when (currentStage.lowercase()) { + "rookie" -> rookieCharacters + "champion" -> championCharacters + "ultimate" -> ultimateCharacters + "mega" -> megaCharacters + else -> rookieCharacters + } + ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded } @@ -1410,7 +1438,7 @@ fun BattlesScreen() { expanded = expanded, onDismissRequest = { expanded = false } ) { - characterList.forEach { character -> + characterListForStage.forEach { character -> DropdownMenuItem( text = { Text(character.name) }, onClick = { From b74b04cda96562e0a9deab37620c96a856be51a2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 19:50:37 -0400 Subject: [PATCH 44/89] Fixed sprite positioning on main screen. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5e5616e..763716e 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 @@ -498,7 +498,7 @@ fun MiddleBattleView( .size(80.dp) .offset( x = enemyHitOffset, - y = enemyVerticalOffset + y = enemyVerticalOffset + 40.dp ), contentScale = ContentScale.Fit, reloadMappings = false @@ -578,7 +578,7 @@ fun MiddleBattleView( .scale(-1f, 1f) // Flip player Digimon horizontally .offset( x = playerHitOffset, - y = playerVerticalOffset + y = playerVerticalOffset - 40.dp ), contentScale = ContentScale.Fit, reloadMappings = false From 26842d1b1b62f7435e0883a1cd56394ce4ee89b3 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 19:55:36 -0400 Subject: [PATCH 45/89] Reduced attack button size. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 763716e..891bd20 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 @@ -742,15 +742,15 @@ fun MiddleBattleView( }, enabled = battleSystem.isAttackButtonEnabled, modifier = Modifier - .fillMaxWidth() - .height(50.dp), + .fillMaxWidth(0.5f) + .height(35.dp), colors = ButtonDefaults.buttonColors( - containerColor = Color.Red, + containerColor = Color.Blue, disabledContainerColor = Color.Gray ), shape = RoundedCornerShape(8.dp) ) { - Text("Attack", color = Color.White, fontSize = 18.sp) + Text("Attack", color = Color.White, fontSize = 12.sp) } } } From ec3058b511f13bae18f03c2e340eb98709fba083 Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 20:03:50 -0400 Subject: [PATCH 46/89] Adjusted dodge trigger. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 891bd20..9aba1e8 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 @@ -116,8 +116,8 @@ fun BattleScreen( progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the enemy (around 55% progress for enemy dodge) - if (progress >= 0.55f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { + // Trigger animation when attack reaches the enemy (around 40% progress for enemy dodge) + if (progress >= 0.50f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { if (battleSystem.attackIsHit) { // Player attack hits enemy println("Player attack hits enemy at progress $progress") @@ -155,8 +155,8 @@ fun BattleScreen( progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the player (around 75% progress for player dodge) - if (progress >= 0.75f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { + // Trigger animation when attack reaches the player (around 60% progress for player dodge) + if (progress >= 0.50f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { println("Phase 3: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}") println("Phase 3: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") if (battleSystem.opponentAttackIsHit) { From 6ea9946412b1b167993c25d6eda481afed4bc9c1 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 09:00:51 -0400 Subject: [PATCH 47/89] Attack sprites now shows on main battle screen. --- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 9aba1e8..3decf0b 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 @@ -460,9 +460,9 @@ fun MiddleBattleView( ) { // Determine animation type for enemy val enemyAnimationType = when { + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1 battleSystem.isOpponentDodging -> DigimonAnimationType.WALK battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP - battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking else -> DigimonAnimationType.IDLE } @@ -506,7 +506,8 @@ fun MiddleBattleView( // Enemy attack sprite (Phase 1 only) if (battleSystem.attackPhase == 1) { - val xOffset = (attackAnimationProgress * 400 + 50).dp // Move right off screen + val xOffset = (-attackAnimationProgress * 400).dp // Start at center, move left off screen + val yOffset = 30.dp // Lower enemy attack sprite by 30 pixels AttackSpriteImage( characterId = opponentCharacter?.charaId ?: "dim011_mon01", @@ -515,7 +516,7 @@ fun MiddleBattleView( .size(60.dp) .offset( x = xOffset, - y = 0.dp + y = yOffset ), contentScale = ContentScale.Fit ) @@ -539,9 +540,9 @@ fun MiddleBattleView( ) { // Determine animation type for player val playerAnimationType = when { + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1 battleSystem.isPlayerDodging -> DigimonAnimationType.WALK battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP - battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking else -> DigimonAnimationType.IDLE } @@ -586,7 +587,8 @@ fun MiddleBattleView( // Player attack sprite (Phase 1 only) if (battleSystem.attackPhase == 1) { - val xOffset = (-attackAnimationProgress * 400 - 50).dp // Move left off screen + val xOffset = (attackAnimationProgress * 400).dp // Start at center, move right off screen + val yOffset = (-30).dp // Raise player attack sprite by 30 pixels AttackSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", @@ -595,7 +597,7 @@ fun MiddleBattleView( .size(60.dp) .offset( x = xOffset, - y = 0.dp + y = yOffset ) .scale(-1f, 1f), // Flip attack sprite contentScale = ContentScale.Fit @@ -722,6 +724,8 @@ fun MiddleBattleView( onSetPendingDamage(0f, 0f) // No damage } battleSystem.setupCounterAttack(finalCounterAttackHits) + // Set the opponent attack hit state for Phase 3 + battleSystem.handleOpponentAttackResult(finalCounterAttackHits) } } 2 -> { @@ -1047,6 +1051,8 @@ fun PlayerBattleView( onSetPendingDamage(0f, 0f) // No damage } battleSystem.setupCounterAttack(finalCounterAttackHits) + // Set the opponent attack hit state for Phase 3 + battleSystem.handleOpponentAttackResult(finalCounterAttackHits) } } 2 -> { From cd33d06ecfc8983b299237973cbf45277d3b1b45 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 10:25:26 -0400 Subject: [PATCH 48/89] Added pop up damage number. --- .../vbhelper/screens/BattlesScreen.kt | 173 +++++++++++++++++- 1 file changed, 169 insertions(+), 4 deletions(-) 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 3decf0b..44cc3cb 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 @@ -56,8 +56,77 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import kotlin.math.sin import kotlin.math.PI +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +@Composable +fun AnimatedDamageNumber( + damage: Int, + isVisible: Boolean, + modifier: Modifier = Modifier +) { + if (!isVisible) return + + println("DEBUG: AnimatedDamageNumber called with damage=$damage, isVisible=$isVisible") + + var animationProgress by remember { mutableStateOf(0f) } + var scale by remember { mutableStateOf(1f) } + var alpha by remember { mutableStateOf(1f) } + var yOffset by remember { mutableStateOf(0.dp) } + + LaunchedEffect(isVisible) { + if (isVisible) { + println("DEBUG: Starting damage number animation for damage=$damage") + // Start animation + animationProgress = 0f + scale = 0.5f + alpha = 1f + yOffset = 0.dp + + // Animate scale up + while (scale < 1.5f) { + scale += 0.1f + delay(16) + } + + // Hold at max scale briefly + delay(200) + + // Animate fade out and move up + while (alpha > 0f) { + alpha -= 0.05f + yOffset -= 1.dp + delay(16) + } + println("DEBUG: Damage number animation completed for damage=$damage") + } + } + + Text( + text = "-$damage", + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = Color.Red, + textAlign = TextAlign.Center, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(2f, 2f), + blurRadius = 4f + ) + ), + modifier = modifier + .scale(scale) + .alpha(alpha) + .offset(y = yOffset) + ) +} + @Composable fun BattleScreen( stage: String, @@ -82,6 +151,12 @@ fun BattleScreen( var pendingPlayerDamage by remember { mutableStateOf(0f) } var pendingOpponentDamage by remember { mutableStateOf(0f) } + // Damage number animation state + var showPlayerDamageNumber by remember { mutableStateOf(false) } + var showOpponentDamageNumber by remember { mutableStateOf(false) } + var playerDamageValue by remember { mutableStateOf(0) } + var opponentDamageValue by remember { mutableStateOf(0) } + // Critical bar timer LaunchedEffect(Unit) { while (true) { @@ -132,9 +207,7 @@ fun BattleScreen( delay(16) // 60 FPS } println("Phase 2 completed, applying damage and starting Phase 3") - // Apply player's damage battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) - pendingOpponentDamage = 0f delay(500) // Check if there should be a counter-attack @@ -173,9 +246,8 @@ fun BattleScreen( delay(16) // 60 FPS } println("Phase 3 completed, applying damage and resetting") - // Apply enemy's damage and reset + println("DEBUG: pendingPlayerDamage = $pendingPlayerDamage") battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) - pendingPlayerDamage = 0f battleSystem.resetAttackState() battleSystem.enableAttackButton() @@ -294,6 +366,45 @@ fun BattleScreen( } } + // Damage number handling + LaunchedEffect(pendingPlayerDamage) { + if (pendingPlayerDamage > 0) { + println("DEBUG: LaunchedEffect triggered for pendingPlayerDamage = $pendingPlayerDamage") + playerDamageValue = pendingPlayerDamage.toInt() + showPlayerDamageNumber = true + println("DEBUG: Setting player damage number: $playerDamageValue, showPlayerDamageNumber = $showPlayerDamageNumber") + + // Hide damage number after animation + delay(3000) // Increased delay to ensure UI recomposition + showPlayerDamageNumber = false + println("DEBUG: Hiding player damage number after delay") + + // Reset pending damage after animation completes + delay(500) // Additional delay before reset + pendingPlayerDamage = 0f + println("DEBUG: Resetting pendingPlayerDamage to 0") + } + } + + LaunchedEffect(pendingOpponentDamage) { + if (pendingOpponentDamage > 0) { + println("DEBUG: LaunchedEffect triggered for pendingOpponentDamage = $pendingOpponentDamage") + opponentDamageValue = pendingOpponentDamage.toInt() + showOpponentDamageNumber = true + println("DEBUG: Setting opponent damage number: $opponentDamageValue, showOpponentDamageNumber = $showOpponentDamageNumber") + + // Hide damage number after animation + delay(3000) // Increased delay to ensure UI recomposition + showOpponentDamageNumber = false + println("DEBUG: Hiding opponent damage number after delay") + + // Reset pending damage after animation completes + delay(500) // Additional delay before reset + pendingOpponentDamage = 0f + println("DEBUG: Resetting pendingOpponentDamage to 0") + } + } + Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -352,6 +463,48 @@ fun BattleScreen( ) } } + + // Damage number overlays - moved inside the Box for proper positioning + when (battleSystem.currentView) { + 0 -> { + // Middle screen - NO damage numbers should show here + // This screen is for the initial attack phase only + } + 1 -> { + // Player screen - show player damage (when opponent attacks player) + println("DEBUG: Player screen damage overlay - playerDamageValue=$playerDamageValue, showPlayerDamageNumber=$showPlayerDamageNumber") + AnimatedDamageNumber( + damage = playerDamageValue, + isVisible = showPlayerDamageNumber, + modifier = Modifier + .align(Alignment.Center) + .offset(y = 0.dp) + .background(Color.Yellow.copy(alpha = 0.3f)) // Debug background + ) + + // Debug text overlay + Text( + text = "View: ${battleSystem.currentView}, Player Damage: $playerDamageValue, Show: $showPlayerDamageNumber", + color = Color.Red, + fontSize = 12.sp, + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = 200.dp) + .background(Color.White.copy(alpha = 0.8f)) + ) + } + 2 -> { + // Enemy screen - show opponent damage (when player attacks opponent) + println("DEBUG: Enemy screen damage overlay - opponentDamageValue=$opponentDamageValue, showOpponentDamageNumber=$showOpponentDamageNumber") + AnimatedDamageNumber( + damage = opponentDamageValue, + isVisible = showOpponentDamageNumber, + modifier = Modifier + .align(Alignment.Center) + .offset(y = (-100).dp) + ) + } + } } } @@ -702,6 +855,12 @@ fun MiddleBattleView( println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage battleSystem.setAttackHitState(true) + + // Also check if enemy counter-attacks and hits + if (apiResult.opponentAttackDamage > 0) { + println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage + } } else { // Player attack missed - enemy counter-attacks println("Player attack missed! Enemy counter-attacks") @@ -1029,6 +1188,12 @@ fun PlayerBattleView( println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage battleSystem.setAttackHitState(true) + + // Also check if enemy counter-attacks and hits + if (apiResult.opponentAttackDamage > 0) { + println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage + } } else { // Player attack missed - enemy counter-attacks println("Player attack missed! Enemy counter-attacks") From de3d312a325636dd7215e5caa7f722bc6c28e2eb Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 10:49:30 -0400 Subject: [PATCH 49/89] Fixed pop up damage timing and positioning. --- .../vbhelper/screens/BattlesScreen.kt | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) 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 44cc3cb..b477c8f 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 @@ -191,12 +191,17 @@ fun BattleScreen( progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the enemy (around 40% progress for enemy dodge) + // Trigger animation when attack reaches the enemy (around 50% progress for enemy dodge) if (progress >= 0.50f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { if (battleSystem.attackIsHit) { // Player attack hits enemy println("Player attack hits enemy at progress $progress") battleSystem.startOpponentHit() + // Show damage number when attack reaches enemy + if (pendingOpponentDamage > 0) { + showOpponentDamageNumber = true + println("DEBUG: Showing opponent damage number at progress $progress") + } } else { // Player attack misses, enemy dodges println("Player attack misses, enemy dodges at progress $progress") @@ -208,7 +213,16 @@ fun BattleScreen( } println("Phase 2 completed, applying damage and starting Phase 3") battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) - delay(500) + + // Hide damage number and reset pending damage after animation + if (showOpponentDamageNumber) { + delay(800) // Wait for damage number animation (scale up + hold + fade out) + showOpponentDamageNumber = false + pendingOpponentDamage = 0f + println("DEBUG: Hiding opponent damage number and resetting pending damage") + } + + delay(200) // Check if there should be a counter-attack if (battleSystem.shouldCounterAttack) { @@ -228,7 +242,7 @@ fun BattleScreen( progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) - // Trigger animation when attack reaches the player (around 60% progress for player dodge) + // Trigger animation when attack reaches the player (around 50% progress for player dodge) if (progress >= 0.50f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { println("Phase 3: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}") println("Phase 3: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") @@ -236,6 +250,11 @@ fun BattleScreen( // Enemy attack hits player println("Enemy attack hits player at progress $progress") battleSystem.startPlayerHit() + // Show damage number when attack reaches player + if (pendingPlayerDamage > 0) { + showPlayerDamageNumber = true + println("DEBUG: Showing player damage number at progress $progress") + } } else { // Enemy attack misses, player dodges println("Enemy attack misses, player dodges at progress $progress") @@ -248,6 +267,15 @@ fun BattleScreen( println("Phase 3 completed, applying damage and resetting") println("DEBUG: pendingPlayerDamage = $pendingPlayerDamage") battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + + // Hide damage number and reset pending damage after animation + if (showPlayerDamageNumber) { + delay(800) // Wait for damage number animation (scale up + hold + fade out) + showPlayerDamageNumber = false + pendingPlayerDamage = 0f + println("DEBUG: Hiding player damage number and resetting pending damage") + } + battleSystem.resetAttackState() battleSystem.enableAttackButton() @@ -366,23 +394,12 @@ fun BattleScreen( } } - // Damage number handling + // Damage number handling - store pending damage but don't show immediately LaunchedEffect(pendingPlayerDamage) { if (pendingPlayerDamage > 0) { println("DEBUG: LaunchedEffect triggered for pendingPlayerDamage = $pendingPlayerDamage") playerDamageValue = pendingPlayerDamage.toInt() - showPlayerDamageNumber = true - println("DEBUG: Setting player damage number: $playerDamageValue, showPlayerDamageNumber = $showPlayerDamageNumber") - - // Hide damage number after animation - delay(3000) // Increased delay to ensure UI recomposition - showPlayerDamageNumber = false - println("DEBUG: Hiding player damage number after delay") - - // Reset pending damage after animation completes - delay(500) // Additional delay before reset - pendingPlayerDamage = 0f - println("DEBUG: Resetting pendingPlayerDamage to 0") + // Don't show immediately - wait for attack animation to reach the Digimon } } @@ -390,18 +407,7 @@ fun BattleScreen( if (pendingOpponentDamage > 0) { println("DEBUG: LaunchedEffect triggered for pendingOpponentDamage = $pendingOpponentDamage") opponentDamageValue = pendingOpponentDamage.toInt() - showOpponentDamageNumber = true - println("DEBUG: Setting opponent damage number: $opponentDamageValue, showOpponentDamageNumber = $showOpponentDamageNumber") - - // Hide damage number after animation - delay(3000) // Increased delay to ensure UI recomposition - showOpponentDamageNumber = false - println("DEBUG: Hiding opponent damage number after delay") - - // Reset pending damage after animation completes - delay(500) // Additional delay before reset - pendingOpponentDamage = 0f - println("DEBUG: Resetting pendingOpponentDamage to 0") + // Don't show immediately - wait for attack animation to reach the Digimon } } @@ -478,11 +484,12 @@ fun BattleScreen( isVisible = showPlayerDamageNumber, modifier = Modifier .align(Alignment.Center) - .offset(y = 0.dp) - .background(Color.Yellow.copy(alpha = 0.3f)) // Debug background + .offset(y = (-50).dp) + //.background(Color.Yellow.copy(alpha = 0.3f)) // Debug background ) // Debug text overlay + /* Text( text = "View: ${battleSystem.currentView}, Player Damage: $playerDamageValue, Show: $showPlayerDamageNumber", color = Color.Red, @@ -492,6 +499,7 @@ fun BattleScreen( .offset(y = 200.dp) .background(Color.White.copy(alpha = 0.8f)) ) + */ } 2 -> { // Enemy screen - show opponent damage (when player attacks opponent) @@ -501,7 +509,7 @@ fun BattleScreen( isVisible = showOpponentDamageNumber, modifier = Modifier .align(Alignment.Center) - .offset(y = (-100).dp) + .offset(y = (-50).dp) ) } } From 5989f48355e80997de46773b31e5b5c2159309e6 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 11:51:37 -0400 Subject: [PATCH 50/89] Added battle background. --- .../vbhelper/screens/BattlesScreen.kt | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) 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 b477c8f..cb1d3d6 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 @@ -62,7 +62,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.text.TextStyle - +import androidx.compose.foundation.Image +import androidx.compose.ui.graphics.asImageBitmap +import android.graphics.BitmapFactory +import java.io.File +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalConfiguration @Composable fun AnimatedDamageNumber( @@ -222,7 +227,7 @@ fun BattleScreen( println("DEBUG: Hiding opponent damage number and resetting pending damage") } - delay(200) + delay(100) // Check if there should be a counter-attack if (battleSystem.shouldCounterAttack) { @@ -539,6 +544,11 @@ fun MiddleBattleView( Box( modifier = Modifier.fillMaxSize() ) { + // Animated background - positioned underneath all other sprites + AnimatedBattleBackground( + modifier = Modifier.fillMaxSize() + ) + // Top section: Exit button, HP bars, and HP numbers Column( modifier = Modifier @@ -2106,4 +2116,80 @@ fun BattlesScreen() { } } } +} + +@Composable +fun AnimatedBattleBackground( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var backgroundBitmap by remember { mutableStateOf(null) } + var xOffset by remember { mutableStateOf(0f) } + var screenWidth by remember { mutableStateOf(0.dp) } + var screenHeight by remember { mutableStateOf(0.dp) } + + // Get screen dimensions + val density = LocalDensity.current + val configuration = LocalConfiguration.current + LaunchedEffect(Unit) { + screenWidth = with(density) { configuration.screenWidthDp.dp } + screenHeight = with(density) { configuration.screenHeightDp.dp } + println("DEBUG: Screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") + } + + // Load background image from internal storage + LaunchedEffect(Unit) { + try { + val backgroundFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + if (backgroundFile.exists()) { + backgroundBitmap = BitmapFactory.decodeFile(backgroundFile.absolutePath) + println("Successfully loaded battle background: ${backgroundFile.absolutePath}") + println("DEBUG: Image dimensions = ${backgroundBitmap?.width}x${backgroundBitmap?.height} pixels") + } else { + println("Battle background file not found: ${backgroundFile.absolutePath}") + } + } catch (e: Exception) { + println("Error loading battle background: ${e.message}") + } + } + + // Animate horizontal movement to the left with perfect loop + LaunchedEffect(screenWidth) { + if (screenWidth > 0.dp) { + while (true) { + delay(50) // Update every 50ms for smooth animation + xOffset -= 1f // Move 1 pixel to the left + + // Create perfect loop by resetting when one full screen width has moved + if (xOffset <= -screenWidth.value) { + xOffset = 0f + println("DEBUG: Background loop reset at xOffset = ${xOffset}") + } + } + } + } + + backgroundBitmap?.let { bitmap -> + Box(modifier = modifier.fillMaxSize()) { + // First image (main) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Animated Battle Background 1", + modifier = Modifier + .fillMaxSize() + .offset(x = xOffset.dp), + contentScale = ContentScale.FillBounds + ) + + // Second image (for seamless loop) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Animated Battle Background 2", + modifier = Modifier + .fillMaxSize() + .offset(x = (xOffset + screenWidth.value).dp), + contentScale = ContentScale.FillBounds + ) + } + } } \ No newline at end of file From 0a643053af42ae04c91614adbd6ccda9307270bb Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 12:21:30 -0400 Subject: [PATCH 51/89] Adjusted HP text color and size. --- .../vbhelper/screens/BattlesScreen.kt | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) 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 cb1d3d6..212047e 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 @@ -595,8 +595,15 @@ fun MiddleBattleView( // Enemy HP display numbers Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black + fontSize = 16.sp, + color = Color.LightGray, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(2f, 2f), + //blurRadius = 2f + ) + ) ) Spacer(modifier = Modifier.height(16.dp)) @@ -811,8 +818,15 @@ fun MiddleBattleView( // Player HP display numbers Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black + fontSize = 16.sp, + color = Color.LightGray, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(2f, 2f), + //blurRadius = 2f + ) + ) ) Spacer(modifier = Modifier.height(6.dp)) @@ -959,6 +973,9 @@ fun PlayerBattleView( Box( modifier = Modifier.fillMaxSize() ) { + // Animated Battle Background + AnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + // Top section: Exit button, HP bar, and HP numbers Column( modifier = Modifier @@ -1005,8 +1022,15 @@ fun PlayerBattleView( // Health display numbers Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black + fontSize = 16.sp, + color = Color.LightGray, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(2f, 2f), + //blurRadius = 2f + ) + ) ) } @@ -1287,6 +1311,9 @@ fun EnemyBattleView( Box( modifier = Modifier.fillMaxSize() ) { + // Animated Battle Background + AnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + // Top section: Enemy HP bar and HP numbers Column( modifier = Modifier @@ -1306,8 +1333,15 @@ fun EnemyBattleView( // Enemy HP display numbers Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black + fontSize = 16.sp, + color = Color.LightGray, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(2f, 2f), + //blurRadius = 2f + ) + ) ) } From 28cb824bf319cb762b8a71c054b243eb3749d4aa Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 12:36:21 -0400 Subject: [PATCH 52/89] Changed battle background to use all 3 layers. --- .../vbhelper/screens/BattlesScreen.kt | 175 ++++++++++++++++-- 1 file changed, 162 insertions(+), 13 deletions(-) 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 212047e..c78b0db 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 @@ -545,7 +545,7 @@ fun MiddleBattleView( modifier = Modifier.fillMaxSize() ) { // Animated background - positioned underneath all other sprites - AnimatedBattleBackground( + MultiLayerAnimatedBattleBackground( modifier = Modifier.fillMaxSize() ) @@ -596,11 +596,11 @@ fun MiddleBattleView( Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", fontSize = 16.sp, - color = Color.LightGray, + color = Color.White, style = TextStyle( shadow = Shadow( color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(2f, 2f), + offset = androidx.compose.ui.geometry.Offset(4f, 4f), //blurRadius = 2f ) ) @@ -819,11 +819,11 @@ fun MiddleBattleView( Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", fontSize = 16.sp, - color = Color.LightGray, + color = Color.White, style = TextStyle( shadow = Shadow( color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(2f, 2f), + offset = androidx.compose.ui.geometry.Offset(4f, 4f), //blurRadius = 2f ) ) @@ -973,8 +973,8 @@ fun PlayerBattleView( Box( modifier = Modifier.fillMaxSize() ) { - // Animated Battle Background - AnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + // Multi-layer animated battle background + MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) // Top section: Exit button, HP bar, and HP numbers Column( @@ -1023,11 +1023,11 @@ fun PlayerBattleView( Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", fontSize = 16.sp, - color = Color.LightGray, + color = Color.White, style = TextStyle( shadow = Shadow( color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(2f, 2f), + offset = androidx.compose.ui.geometry.Offset(4f, 4f), //blurRadius = 2f ) ) @@ -1311,8 +1311,8 @@ fun EnemyBattleView( Box( modifier = Modifier.fillMaxSize() ) { - // Animated Battle Background - AnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + // Multi-layer animated battle background + MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) // Top section: Enemy HP bar and HP numbers Column( @@ -1334,11 +1334,11 @@ fun EnemyBattleView( Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", fontSize = 16.sp, - color = Color.LightGray, + color = Color.White, style = TextStyle( shadow = Shadow( color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(2f, 2f), + offset = androidx.compose.ui.geometry.Offset(4f, 4f), //blurRadius = 2f ) ) @@ -2226,4 +2226,153 @@ fun AnimatedBattleBackground( ) } } +} + +@Composable +fun MultiLayerAnimatedBattleBackground( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var backLayerBitmap by remember { mutableStateOf(null) } + var middleLayerBitmap by remember { mutableStateOf(null) } + var frontLayerBitmap by remember { mutableStateOf(null) } + + var backLayerXOffset by remember { mutableStateOf(0f) } + var middleLayerXOffset by remember { mutableStateOf(0f) } + var frontLayerXOffset by remember { mutableStateOf(0f) } + + var screenWidth by remember { mutableStateOf(0.dp) } + var screenHeight by remember { mutableStateOf(0.dp) } + + // Get screen dimensions + val density = LocalDensity.current + val configuration = LocalConfiguration.current + LaunchedEffect(Unit) { + screenWidth = with(density) { configuration.screenWidthDp.dp } + screenHeight = with(density) { configuration.screenHeightDp.dp } + println("DEBUG: Multi-layer screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") + } + + // Load all three background layers from internal storage + LaunchedEffect(Unit) { + try { + // Back layer (BattleBg_0018_BattleBg_0013.png) + val backLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0018_BattleBg_0013.png") + if (backLayerFile.exists()) { + backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) + println("Successfully loaded back layer background: ${backLayerFile.absolutePath}") + } else { + println("Back layer background file not found: ${backLayerFile.absolutePath}") + } + + // Middle layer (BattleBg_0015_BattleBg_0012.png) + val middleLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + if (middleLayerFile.exists()) { + middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) + println("Successfully loaded middle layer background: ${middleLayerFile.absolutePath}") + } else { + println("Middle layer background file not found: ${middleLayerFile.absolutePath}") + } + + // Front layer (BattleBg_0005_BattleBg_0011.png) + val frontLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0005_BattleBg_0011.png") + if (frontLayerFile.exists()) { + frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) + println("Successfully loaded front layer background: ${frontLayerFile.absolutePath}") + } else { + println("Front layer background file not found: ${frontLayerFile.absolutePath}") + } + } catch (e: Exception) { + println("Error loading multi-layer battle backgrounds: ${e.message}") + } + } + + // Animate all three layers with different speeds + LaunchedEffect(screenWidth) { + if (screenWidth > 0.dp) { + while (true) { + delay(50) // Update every 50ms for smooth animation + + // Back layer moves slowest (parallax effect) + backLayerXOffset -= 0.5f + if (backLayerXOffset <= -screenWidth.value) { + backLayerXOffset = 0f + } + + // Middle layer moves at medium speed + middleLayerXOffset -= 1f + if (middleLayerXOffset <= -screenWidth.value) { + middleLayerXOffset = 0f + } + + // Front layer moves fastest + frontLayerXOffset -= 1.5f + if (frontLayerXOffset <= -screenWidth.value) { + frontLayerXOffset = 0f + } + } + } + } + + Box(modifier = modifier.fillMaxSize()) { + // Back layer (underneath everything) + backLayerBitmap?.let { bitmap -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Back Layer Battle Background 1", + modifier = Modifier + .fillMaxSize() + .offset(x = backLayerXOffset.dp), + contentScale = ContentScale.FillBounds + ) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Back Layer Battle Background 2", + modifier = Modifier + .fillMaxSize() + .offset(x = (backLayerXOffset + screenWidth.value).dp), + contentScale = ContentScale.FillBounds + ) + } + + // Middle layer + middleLayerBitmap?.let { bitmap -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Middle Layer Battle Background 1", + modifier = Modifier + .fillMaxSize() + .offset(x = middleLayerXOffset.dp), + contentScale = ContentScale.FillBounds + ) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Middle Layer Battle Background 2", + modifier = Modifier + .fillMaxSize() + .offset(x = (middleLayerXOffset + screenWidth.value).dp), + contentScale = ContentScale.FillBounds + ) + } + + // Front layer (on top of other backgrounds) + frontLayerBitmap?.let { bitmap -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Front Layer Battle Background 1", + modifier = Modifier + .fillMaxSize() + .offset(x = frontLayerXOffset.dp), + contentScale = ContentScale.FillBounds + ) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Front Layer Battle Background 2", + modifier = Modifier + .fillMaxSize() + .offset(x = (frontLayerXOffset + screenWidth.value).dp), + contentScale = ContentScale.FillBounds + ) + } + } } \ No newline at end of file From d833a89c170c7540a2db4b98a6b970bc0d2a6e0a Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 18:03:03 -0400 Subject: [PATCH 53/89] Added hit sprites. --- .../vbhelper/battle/HitEffectComposables.kt | 103 ++++++++++ .../vbhelper/battle/HitEffectSpriteManager.kt | 176 ++++++++++++++++++ .../vbhelper/screens/BattlesScreen.kt | 51 +++++ 3 files changed, 330 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt new file mode 100644 index 0000000..7ccf1c6 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt @@ -0,0 +1,103 @@ +package com.github.nacabaro.vbhelper.battle + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun HitEffectOverlay( + isVisible: Boolean, + modifier: Modifier = Modifier, + isPlayerScreen: Boolean = false, + onAnimationComplete: () -> Unit = {} +) { + if (!isVisible) return + + val context = LocalContext.current + val hitEffectManager = remember { HitEffectSpriteManager(context) } + val coroutineScope = rememberCoroutineScope() + + var currentFrame by remember { mutableStateOf(0) } + var currentSprite by remember { mutableStateOf(null) } + var animationProgress by remember { mutableStateOf(0f) } + var scale by remember { mutableStateOf(0.5f) } + var alpha by remember { mutableStateOf(1f) } + + LaunchedEffect(isVisible) { + if (isVisible) { + println("DEBUG: Starting hit effect animation") + + // Randomly choose between hit_01, hit_02, and hit_02_white + val hitSpriteName = when (kotlin.random.Random.nextInt(3)) { + 0 -> "hit_01" + 1 -> "hit_02" + else -> "hit_02_white" + } + currentSprite = hitEffectManager.loadHitSprite(hitSpriteName) + + if (currentSprite != null) { + // Animate the hit effect + animationProgress = 0f + scale = 0.5f + alpha = 1f + + // Scale up animation - slowed down + while (scale < 1.2f) { + scale += 0.05f // Reduced from 0.1f to 0.05f + delay(32) // Increased from 16ms to 32ms + } + + // Hold for a moment - increased duration + delay(300) // Increased from 100ms to 300ms + + // Fade out - slowed down + while (alpha > 0f) { + alpha -= 0.03f // Reduced from 0.05f to 0.03f + delay(32) // Increased from 16ms to 32ms + } + + println("DEBUG: Hit effect animation completed") + onAnimationComplete() + } else { + println("DEBUG: Failed to load hit sprite") + onAnimationComplete() + } + } + } + + currentSprite?.let { sprite -> + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + bitmap = sprite.asImageBitmap(), + contentDescription = "Hit Effect", + modifier = Modifier + .size((sprite.width * scale).dp, (sprite.height * scale).dp) + .offset( + x = if (isPlayerScreen) { + // On player screen, position further to the left + (-sprite.width * scale / 2 - 100).dp + } else { + // On enemy screen, position further to the right + (-sprite.width * scale / 2 + 150).dp + }, + y = (-sprite.height * scale / 2 + 40).dp // Position lower on screen (was -60, now +40) + ), + contentScale = ContentScale.Fit + ) + } + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt new file mode 100644 index 0000000..33d2189 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -0,0 +1,176 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Rect +import java.io.File + +class HitEffectSpriteManager(private val context: Context) { + private val spriteCache = mutableMapOf() + + // Base directory where hit effect sprites are stored + private val hitSpritesDir = File(context.filesDir, "battle_sprites/extracted_hit_sprites") + + /** + * Load a hit sprite (hit_01.png, hit_02.png, hit_02_white.png) + * @param spriteName The sprite name (e.g., "hit_01", "hit_02", "hit_02_white") + * @return Bitmap of the hit sprite, or null if not found + */ + fun loadHitSprite(spriteName: String): Bitmap? { + val cacheKey = "hit_$spriteName" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + try { + val spriteFile = File(hitSpritesDir, "$spriteName.png") + + if (!spriteFile.exists()) { + println("Hit sprite file not found: ${spriteFile.absolutePath}") + return null + } + + val bitmap = BitmapFactory.decodeFile(spriteFile.absolutePath) + if (bitmap == null) { + println("Failed to decode hit sprite file: ${spriteFile.absolutePath}") + return null + } + + println("Successfully loaded hit sprite: $spriteName.png (${bitmap.width}x${bitmap.height})") + + // Cache the result + spriteCache[cacheKey] = bitmap + + return bitmap + + } catch (e: Exception) { + println("Error loading hit sprite: ${e.message}") + e.printStackTrace() + return null + } + } + + /** + * Load a damage effect sprite from spritesheet + * @param spritesheetName The spritesheet name (e.g., "dmg_ef1", "dmg_ef2") + * @param frameIndex The frame index (0-3 for dmg_ef1 and dmg_ef2, 0 for dmg_ef3) + * @return Bitmap of the damage effect frame, or null if not found + */ + fun loadDamageEffectSprite(spritesheetName: String, frameIndex: Int = 0): Bitmap? { + val cacheKey = "dmg_${spritesheetName}_frame_${frameIndex}" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + try { + val spritesheetFile = File(hitSpritesDir, "$spritesheetName.png") + + if (!spritesheetFile.exists()) { + println("Damage effect spritesheet not found: ${spritesheetFile.absolutePath}") + return null + } + + val spritesheet = BitmapFactory.decodeFile(spritesheetFile.absolutePath) + if (spritesheet == null) { + println("Failed to decode damage effect spritesheet: ${spritesheetFile.absolutePath}") + return null + } + + // Extract frame from spritesheet + val frameBitmap = when (spritesheetName) { + "dmg_ef1", "dmg_ef2" -> { + // These are 2x2 spritesheets (4 frames) + val frameWidth = spritesheet.width / 2 + val frameHeight = spritesheet.height / 2 + val row = frameIndex / 2 + val col = frameIndex % 2 + val x = col * frameWidth + val y = row * frameHeight + Bitmap.createBitmap(spritesheet, x, y, frameWidth, frameHeight) + } + "dmg_ef3" -> { + // This is a single sprite + spritesheet + } + else -> { + println("Unknown spritesheet name: $spritesheetName") + return null + } + } + + println("Successfully loaded damage effect frame: $spritesheetName frame $frameIndex (${frameBitmap.width}x${frameBitmap.height})") + + // Cache the result + spriteCache[cacheKey] = frameBitmap + + return frameBitmap + + } catch (e: Exception) { + println("Error loading damage effect sprite: ${e.message}") + e.printStackTrace() + return null + } + } + + /** + * Get available hit sprite names + * @return List of available hit sprite names + */ + fun getAvailableHitSprites(): List { + try { + if (!hitSpritesDir.exists()) { + return emptyList() + } + + val hitFiles = hitSpritesDir.listFiles { file -> + file.name.startsWith("hit_") && file.name.endsWith(".png") + } ?: emptyArray() + + return hitFiles.map { file -> + file.name.substringBefore(".png") + }.sorted() + + } catch (e: Exception) { + println("Error getting available hit sprites: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Get available damage effect spritesheet names + * @return List of available damage effect spritesheet names + */ + fun getAvailableDamageEffectSpritesheets(): List { + try { + if (!hitSpritesDir.exists()) { + return emptyList() + } + + val dmgFiles = hitSpritesDir.listFiles { file -> + file.name.startsWith("dmg_ef") && file.name.endsWith(".png") + } ?: emptyArray() + + return dmgFiles.map { file -> + file.name.substringBefore(".png") + }.sorted() + + } catch (e: Exception) { + println("Error getting available damage effect spritesheets: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Clear the sprite cache + */ + fun clearCache() { + spriteCache.clear() + } +} 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 c78b0db..065308a 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 @@ -52,6 +52,7 @@ import com.github.nacabaro.vbhelper.battle.SpriteFileManager import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage +import com.github.nacabaro.vbhelper.battle.HitEffectOverlay import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import kotlin.math.sin @@ -162,6 +163,20 @@ fun BattleScreen( var playerDamageValue by remember { mutableStateOf(0) } var opponentDamageValue by remember { mutableStateOf(0) } + // Hit effect animation state + var showPlayerHitEffect by remember { mutableStateOf(false) } + var showOpponentHitEffect by remember { mutableStateOf(false) } + + // Reset hit effect states when attack phase returns to idle + LaunchedEffect(battleSystem.attackPhase) { + if (battleSystem.attackPhase == 0) { + // Reset hit effect states when returning to idle + showPlayerHitEffect = false + showOpponentHitEffect = false + println("DEBUG: Reset hit effect states - attack phase returned to idle") + } + } + // Critical bar timer LaunchedEffect(Unit) { while (true) { @@ -190,6 +205,7 @@ fun BattleScreen( 2 -> { // Phase 2: Player attack on enemy screen println("Starting Phase 2: Player attack on enemy screen") + println("DEBUG: Phase 2 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(2) // Enemy screen var progress = 0f while (progress < 1f) { @@ -202,6 +218,9 @@ fun BattleScreen( // Player attack hits enemy println("Player attack hits enemy at progress $progress") battleSystem.startOpponentHit() + // Show hit effect and damage effect + println("DEBUG: Setting showOpponentHitEffect = true (player attack hits enemy)") + showOpponentHitEffect = true // Show damage number when attack reaches enemy if (pendingOpponentDamage > 0) { showOpponentDamageNumber = true @@ -241,6 +260,7 @@ fun BattleScreen( 3 -> { // Phase 3: Enemy attack on player screen println("Starting Phase 3: Enemy attack on player screen") + println("DEBUG: Phase 3 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(1) // Player screen var progress = 0f while (progress < 1f) { @@ -255,6 +275,9 @@ fun BattleScreen( // Enemy attack hits player println("Enemy attack hits player at progress $progress") battleSystem.startPlayerHit() + // Show hit effect and damage effect + println("DEBUG: Setting showPlayerHitEffect = true (enemy attack hits player)") + showPlayerHitEffect = true // Show damage number when attack reaches player if (pendingPlayerDamage > 0) { showPlayerDamageNumber = true @@ -493,6 +516,20 @@ fun BattleScreen( //.background(Color.Yellow.copy(alpha = 0.3f)) // Debug background ) + // Player hit effects + HitEffectOverlay( + isVisible = showPlayerHitEffect, + modifier = Modifier.fillMaxSize(), + isPlayerScreen = true, + onAnimationComplete = { + println("DEBUG: Player hit effect animation completed, setting showPlayerHitEffect = false") + showPlayerHitEffect = false + println("DEBUG: Player hit effect animation completed") + } + ) + + + // Debug text overlay /* Text( @@ -516,6 +553,20 @@ fun BattleScreen( .align(Alignment.Center) .offset(y = (-50).dp) ) + + // Enemy hit effects + HitEffectOverlay( + isVisible = showOpponentHitEffect, + modifier = Modifier.fillMaxSize(), + isPlayerScreen = false, + onAnimationComplete = { + println("DEBUG: Enemy hit effect animation completed, setting showOpponentHitEffect = false") + showOpponentHitEffect = false + println("DEBUG: Enemy hit effect animation completed") + } + ) + + } } } From 371a850d451cf286e2bc8ac5b7bb5bb698188741 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 18:38:11 -0400 Subject: [PATCH 54/89] Offset idle animation so player/enemy Digimon aren't in sync. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 16 ++++++++++++++-- .../vbhelper/battle/DigimonAnimationState.kt | 17 ++++++++++++----- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 15 ++++++++++----- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt index e8c2049..ae872bf 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.launch +import kotlinx.coroutines.delay @Composable fun AnimatedSpriteImage( @@ -14,11 +15,22 @@ fun AnimatedSpriteImage( animationType: DigimonAnimationType = DigimonAnimationType.IDLE, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit, - reloadMappings: Boolean = false + reloadMappings: Boolean = false, + animationOffset: Long = 0L // New parameter for offsetting animation timing ) { val context = LocalContext.current val spriteManager = remember { IndividualSpriteManager(context) } - val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context) } + + // Calculate frame offset based on animation offset + // 750ms is the idle animation duration, so we calculate how many frames to offset + val frameOffset = if (animationOffset > 0L) { + // Convert time offset to frame offset (2 frames per cycle, 750ms per frame) + ((animationOffset / 750L) * 2).toInt() + } else { + 0 + } + + val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context, frameOffset, animationOffset) } val coroutineScope = rememberCoroutineScope() var bitmap by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index 2ff49cd..d23ea11 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -31,7 +31,9 @@ data class AnimationState( class DigimonAnimationStateMachine( private val characterId: String, - private val context: Context + private val context: Context, + private val initialFrameOffset: Int = 0, // New parameter for offsetting the starting frame + private val timingOffset: Long = 0L // New parameter for offsetting the timing ) { var currentAnimation by mutableStateOf(DigimonAnimationType.IDLE) private set @@ -79,7 +81,7 @@ class DigimonAnimationStateMachine( ) init { - println("Initialized DigimonAnimationStateMachine for character: $characterId") + println("Initialized DigimonAnimationStateMachine for character: $characterId with frame offset: $initialFrameOffset, timing offset: $timingOffset") println("Available animation types: ${animationTypeToFrames.keys}") } @@ -128,12 +130,17 @@ class DigimonAnimationStateMachine( // Combine frames for cycling idle animation val combinedFrames = (idleFrames + idle2Frames).distinct() - println("Playing idle animation with frames: $combinedFrames") + println("Playing idle animation with frames: $combinedFrames, starting at offset: $initialFrameOffset, timing offset: $timingOffset") val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L - // Cycle through idle frames - var frameIndex = 0 + // Apply initial timing offset + if (timingOffset > 0L) { + delay(timingOffset) + } + + // Cycle through idle frames, starting from the offset + var frameIndex = initialFrameOffset while (isPlaying && currentAnimation == DigimonAnimationType.IDLE) { val frameNumber = combinedFrames[frameIndex % combinedFrames.size] currentFrameNumber = frameNumber 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 065308a..4711ca5 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 @@ -730,7 +730,8 @@ fun MiddleBattleView( y = enemyVerticalOffset + 40.dp ), contentScale = ContentScale.Fit, - reloadMappings = false + reloadMappings = false, + animationOffset = 375L // Offset enemy animation by half the idle duration ) // Enemy attack sprite (Phase 1 only) @@ -811,7 +812,8 @@ fun MiddleBattleView( y = playerVerticalOffset - 40.dp ), contentScale = ContentScale.Fit, - reloadMappings = false + reloadMappings = false, + animationOffset = 0L // Player animation starts immediately ) // Player attack sprite (Phase 1 only) @@ -1149,7 +1151,8 @@ fun PlayerBattleView( y = verticalOffset ), contentScale = ContentScale.Fit, - reloadMappings = false + reloadMappings = false, + animationOffset = 0L // Player animation starts immediately ) // Attack sprite visibility and positioning based on attack phase @@ -1456,7 +1459,8 @@ fun EnemyBattleView( y = verticalOffset ), contentScale = ContentScale.Fit, - reloadMappings = false + reloadMappings = false, + animationOffset = 375L // Offset enemy animation by half the idle duration ) // Attack sprite visibility and positioning based on attack phase @@ -1797,7 +1801,8 @@ fun BattlesScreen() { animationType = currentTestAnimation, modifier = Modifier.size(120.dp), contentScale = ContentScale.Fit, - reloadMappings = false + reloadMappings = false, + animationOffset = 0L // No offset for sprite tester ) Spacer(modifier = Modifier.height(16.dp)) From 26fc0ced5635906105d561c8e8d33471c9d6a6c6 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 18:53:23 -0400 Subject: [PATCH 55/89] Adjusted hit effect timing. --- .../vbhelper/battle/HitEffectComposables.kt | 3 ++ .../vbhelper/screens/BattlesScreen.kt | 48 ++++++++++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt index 7ccf1c6..5110d1c 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt @@ -38,6 +38,9 @@ fun HitEffectOverlay( if (isVisible) { println("DEBUG: Starting hit effect animation") + // Add delay before starting hit effect animation + delay(400) // Increased from 200ms to 400ms delay before hit effect appears + // Randomly choose between hit_01, hit_02, and hit_02_white val hitSpriteName = when (kotlin.random.Random.nextInt(3)) { 0 -> "hit_01" 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 4711ca5..50ca188 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 @@ -167,12 +167,18 @@ fun BattleScreen( var showPlayerHitEffect by remember { mutableStateOf(false) } var showOpponentHitEffect by remember { mutableStateOf(false) } + // Attack sprite visibility state + var hidePlayerAttackSprite by remember { mutableStateOf(false) } + var hideEnemyAttackSprite by remember { mutableStateOf(false) } + // Reset hit effect states when attack phase returns to idle LaunchedEffect(battleSystem.attackPhase) { if (battleSystem.attackPhase == 0) { // Reset hit effect states when returning to idle showPlayerHitEffect = false showOpponentHitEffect = false + hidePlayerAttackSprite = false + hideEnemyAttackSprite = false println("DEBUG: Reset hit effect states - attack phase returned to idle") } } @@ -221,6 +227,11 @@ fun BattleScreen( // Show hit effect and damage effect println("DEBUG: Setting showOpponentHitEffect = true (player attack hits enemy)") showOpponentHitEffect = true + // Delay hiding the attack sprite to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + hideEnemyAttackSprite = true + } // Show damage number when attack reaches enemy if (pendingOpponentDamage > 0) { showOpponentDamageNumber = true @@ -278,6 +289,11 @@ fun BattleScreen( // Show hit effect and damage effect println("DEBUG: Setting showPlayerHitEffect = true (enemy attack hits player)") showPlayerHitEffect = true + // Delay hiding the attack sprite to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + hidePlayerAttackSprite = true + } // Show damage number when attack reaches player if (pendingPlayerDamage > 0) { showPlayerDamageNumber = true @@ -462,7 +478,9 @@ fun BattleScreen( pendingPlayerDamage = playerDamage pendingOpponentDamage = opponentDamage }, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, + hidePlayerAttackSprite = hidePlayerAttackSprite, + hideEnemyAttackSprite = hideEnemyAttackSprite ) } 1 -> { @@ -482,7 +500,8 @@ fun BattleScreen( pendingPlayerDamage = playerDamage pendingOpponentDamage = opponentDamage }, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, + hidePlayerAttackSprite = hidePlayerAttackSprite ) } 2 -> { @@ -493,7 +512,8 @@ fun BattleScreen( opponentName = opponentName, attackAnimationProgress = battleSystem.attackProgress, activeCharacter = opponentCharacter, - playerCharacter = activeCharacter + playerCharacter = activeCharacter, + hideEnemyAttackSprite = hideEnemyAttackSprite ) } } @@ -524,6 +544,7 @@ fun BattleScreen( onAnimationComplete = { println("DEBUG: Player hit effect animation completed, setting showPlayerHitEffect = false") showPlayerHitEffect = false + hidePlayerAttackSprite = false // Show attack sprite again println("DEBUG: Player hit effect animation completed") } ) @@ -562,6 +583,7 @@ fun BattleScreen( onAnimationComplete = { println("DEBUG: Enemy hit effect animation completed, setting showOpponentHitEffect = false") showOpponentHitEffect = false + hideEnemyAttackSprite = false // Show attack sprite again println("DEBUG: Enemy hit effect animation completed") } ) @@ -584,7 +606,9 @@ fun MiddleBattleView( opponentCharacter: APIBattleCharacter?, context: android.content.Context?, onSetPendingDamage: (Float, Float) -> Unit, - coroutineScope: kotlinx.coroutines.CoroutineScope + coroutineScope: kotlinx.coroutines.CoroutineScope, + hidePlayerAttackSprite: Boolean, + hideEnemyAttackSprite: Boolean ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -735,7 +759,7 @@ fun MiddleBattleView( ) // Enemy attack sprite (Phase 1 only) - if (battleSystem.attackPhase == 1) { + if (battleSystem.attackPhase == 1 && !hideEnemyAttackSprite) { val xOffset = (-attackAnimationProgress * 400).dp // Start at center, move left off screen val yOffset = 30.dp // Lower enemy attack sprite by 30 pixels @@ -817,7 +841,7 @@ fun MiddleBattleView( ) // Player attack sprite (Phase 1 only) - if (battleSystem.attackPhase == 1) { + if (battleSystem.attackPhase == 1 && !hidePlayerAttackSprite) { val xOffset = (attackAnimationProgress * 400).dp // Start at center, move right off screen val yOffset = (-30).dp // Raise player attack sprite by 30 pixels @@ -1015,7 +1039,8 @@ fun PlayerBattleView( context: android.content.Context?, opponent: APIBattleCharacter?, onSetPendingDamage: (Float, Float) -> Unit, - coroutineScope: kotlinx.coroutines.CoroutineScope + coroutineScope: kotlinx.coroutines.CoroutineScope, + hidePlayerAttackSprite: Boolean ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -1190,7 +1215,7 @@ fun PlayerBattleView( println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - if (!isTransitioning) { + if (!isTransitioning && !hidePlayerAttackSprite) { AttackSpriteImage( characterId = characterId, isLarge = true, @@ -1200,7 +1225,7 @@ fun PlayerBattleView( x = xOffset, y = 0.dp ) - .scale(if (battleSystem.attackPhase == 3) 1f else -1f, 1f), // Don't flip enemy attacks + .scale(1f, 1f), // Don't flip enemy attacks on player screen contentScale = ContentScale.Fit ) } @@ -1355,7 +1380,8 @@ fun EnemyBattleView( opponentName: String, attackAnimationProgress: Float, activeCharacter: APIBattleCharacter? = null, - playerCharacter: APIBattleCharacter? = null + playerCharacter: APIBattleCharacter? = null, + hideEnemyAttackSprite: Boolean ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -1490,7 +1516,7 @@ fun EnemyBattleView( println("EnemyBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - if (!isTransitioning) { + if (!isTransitioning && !hideEnemyAttackSprite) { AttackSpriteImage( characterId = characterId, isLarge = true, From 5bb9fe52099818b708d9e7c21b7bb6891c3081f2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 18:59:08 -0400 Subject: [PATCH 56/89] Delayed damage text to show up when attack sprite hits. --- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 50ca188..412fd25 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 @@ -232,10 +232,13 @@ fun BattleScreen( delay(400) // Match the hit effect delay hideEnemyAttackSprite = true } - // Show damage number when attack reaches enemy + // Delay showing damage number to match hit effect timing if (pendingOpponentDamage > 0) { - showOpponentDamageNumber = true - println("DEBUG: Showing opponent damage number at progress $progress") + coroutineScope.launch { + delay(400) // Match the hit effect delay + showOpponentDamageNumber = true + println("DEBUG: Showing opponent damage number after delay") + } } } else { // Player attack misses, enemy dodges @@ -294,10 +297,13 @@ fun BattleScreen( delay(400) // Match the hit effect delay hidePlayerAttackSprite = true } - // Show damage number when attack reaches player + // Delay showing damage number to match hit effect timing if (pendingPlayerDamage > 0) { - showPlayerDamageNumber = true - println("DEBUG: Showing player damage number at progress $progress") + coroutineScope.launch { + delay(400) // Match the hit effect delay + showPlayerDamageNumber = true + println("DEBUG: Showing player damage number after delay") + } } } else { // Enemy attack misses, player dodges From 61a2439f84c2a32cac6d1c9b7e2407f8a9903376 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 19:19:58 -0400 Subject: [PATCH 57/89] Adjusted horizontal sprite shake timing to sync with attack hit. --- .../vbhelper/battle/ArenaBattleSystem.kt | 60 +++++++++++++ .../vbhelper/screens/BattlesScreen.kt | 86 +++++++++++++++++-- 2 files changed, 137 insertions(+), 9 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 6105bd3..61073fe 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 @@ -5,6 +5,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue +import androidx.compose.runtime.State class ArenaBattleSystem { companion object { @@ -85,6 +86,20 @@ class ArenaBattleSystem { private var _isOpponentHit by mutableStateOf(false) val isOpponentHit: Boolean get() = _isOpponentHit + // Delayed hit states for SLEEP animation timing + private var _isPlayerHitDelayed by mutableStateOf(false) + val isPlayerHitDelayed: Boolean get() = _isPlayerHitDelayed + + private var _isOpponentHitDelayed by mutableStateOf(false) + val isOpponentHitDelayed: Boolean get() = _isOpponentHitDelayed + + // Delayed shake states for shake animation timing + private var _isPlayerShakeDelayed by mutableStateOf(false) + val isPlayerShakeDelayed: Boolean get() = _isPlayerShakeDelayed + + private var _isOpponentShakeDelayed by mutableStateOf(false) + val isOpponentShakeDelayed: Boolean get() = _isOpponentShakeDelayed + // Counter-attack tracking private var _shouldCounterAttack by mutableStateOf(false) val shouldCounterAttack: Boolean get() = _shouldCounterAttack @@ -187,6 +202,10 @@ class ArenaBattleSystem { _opponentDodgeDirection = 1f _isPlayerHit = false _isOpponentHit = false + _isPlayerHitDelayed = false + _isOpponentHitDelayed = false + _isPlayerShakeDelayed = false + _isOpponentShakeDelayed = false _shouldCounterAttack = false _counterAttackIsHit = false _opponentAttackIsHit = false @@ -297,12 +316,22 @@ class ArenaBattleSystem { Log.d(TAG, "Started player hit animation") } + fun startPlayerHitDelayed() { + _isPlayerHitDelayed = true + Log.d(TAG, "Started delayed player hit animation") + } + fun endPlayerHit() { _isPlayerHit = false _hitProgress = 0f Log.d(TAG, "Ended player hit animation") } + fun endPlayerHitDelayed() { + _isPlayerHitDelayed = false + Log.d(TAG, "Ended delayed player hit animation") + } + // Opponent-specific hit methods fun startOpponentHit() { _isOpponentHit = true @@ -310,12 +339,43 @@ class ArenaBattleSystem { Log.d(TAG, "Started opponent hit animation") } + fun startOpponentHitDelayed() { + _isOpponentHitDelayed = true + Log.d(TAG, "Started delayed opponent hit animation") + } + fun endOpponentHit() { _isOpponentHit = false _hitProgress = 0f Log.d(TAG, "Ended opponent hit animation") } + fun endOpponentHitDelayed() { + _isOpponentHitDelayed = false + Log.d(TAG, "Ended delayed opponent hit animation") + } + + // Delayed shake methods + fun startPlayerShakeDelayed() { + _isPlayerShakeDelayed = true + Log.d(TAG, "Started delayed player shake animation") + } + + fun endPlayerShakeDelayed() { + _isPlayerShakeDelayed = false + Log.d(TAG, "Ended delayed player shake animation") + } + + fun startOpponentShakeDelayed() { + _isOpponentShakeDelayed = true + Log.d(TAG, "Started delayed opponent shake animation") + } + + fun endOpponentShakeDelayed() { + _isOpponentShakeDelayed = false + Log.d(TAG, "Ended delayed opponent shake animation") + } + // Combined method to handle attack result fun handleAttackResult(isHit: Boolean) { _attackIsHit = isHit 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 412fd25..e0b808d 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 @@ -179,7 +179,11 @@ fun BattleScreen( showOpponentHitEffect = false hidePlayerAttackSprite = false hideEnemyAttackSprite = false - println("DEBUG: Reset hit effect states - attack phase returned to idle") + battleSystem.endPlayerHitDelayed() + battleSystem.endOpponentHitDelayed() + battleSystem.endPlayerShakeDelayed() + battleSystem.endOpponentShakeDelayed() + println("DEBUG: Reset hit effect states, attack sprite visibility, and delayed hit states") } } @@ -240,6 +244,18 @@ fun BattleScreen( println("DEBUG: Showing opponent damage number after delay") } } + // Delay SLEEP animation to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + battleSystem.startOpponentHitDelayed() + println("DEBUG: Starting delayed opponent hit animation") + } + // Delay shake animation to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + battleSystem.startOpponentShakeDelayed() + println("DEBUG: Starting delayed opponent shake animation") + } } else { // Player attack misses, enemy dodges println("Player attack misses, enemy dodges at progress $progress") @@ -305,6 +321,18 @@ fun BattleScreen( println("DEBUG: Showing player damage number after delay") } } + // Delay SLEEP animation to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + battleSystem.startPlayerHitDelayed() + println("DEBUG: Starting delayed player hit animation") + } + // Delay shake animation to match hit effect timing + coroutineScope.launch { + delay(400) // Match the hit effect delay + battleSystem.startPlayerShakeDelayed() + println("DEBUG: Starting delayed player shake animation") + } } else { // Enemy attack misses, player dodges println("Enemy attack misses, player dodges at progress $progress") @@ -424,6 +452,26 @@ fun BattleScreen( } } + // Player delayed shake animation + LaunchedEffect(battleSystem.isPlayerShakeDelayed) { + if (battleSystem.isPlayerShakeDelayed) { + println("Starting delayed player shake animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endPlayerShakeDelayed() + println("Delayed player shake animation completed") + } + } + // Opponent hit animation LaunchedEffect(battleSystem.isOpponentHit) { if (battleSystem.isOpponentHit) { @@ -444,6 +492,26 @@ fun BattleScreen( } } + // Opponent delayed shake animation + LaunchedEffect(battleSystem.isOpponentShakeDelayed) { + if (battleSystem.isOpponentShakeDelayed) { + println("Starting delayed opponent shake animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endOpponentShakeDelayed() + println("Delayed opponent shake animation completed") + } + } + // Damage number handling - store pending damage but don't show immediately LaunchedEffect(pendingPlayerDamage) { if (pendingPlayerDamage > 0) { @@ -721,7 +789,7 @@ fun MiddleBattleView( val enemyAnimationType = when { battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1 battleSystem.isOpponentDodging -> DigimonAnimationType.WALK - battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP + battleSystem.isOpponentHitDelayed -> DigimonAnimationType.SLEEP else -> DigimonAnimationType.IDLE } @@ -741,7 +809,7 @@ fun MiddleBattleView( } // Calculate hit effect for enemy - val enemyHitOffset = if (battleSystem.isOpponentHit) { + val enemyHitOffset = if (battleSystem.isOpponentShakeDelayed) { val shakeAmount = 5.dp val progress = battleSystem.hitProgress val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f @@ -802,7 +870,7 @@ fun MiddleBattleView( val playerAnimationType = when { battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1 battleSystem.isPlayerDodging -> DigimonAnimationType.WALK - battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP + battleSystem.isPlayerHitDelayed -> DigimonAnimationType.SLEEP else -> DigimonAnimationType.IDLE } @@ -822,7 +890,7 @@ fun MiddleBattleView( } // Calculate hit effect for player - val playerHitOffset = if (battleSystem.isPlayerHit) { + val playerHitOffset = if (battleSystem.isPlayerShakeDelayed) { val shakeAmount = 5.dp val progress = battleSystem.hitProgress val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f @@ -1135,7 +1203,7 @@ fun PlayerBattleView( // Determine animation type based on battle state val animationType = when { battleSystem.isPlayerDodging -> DigimonAnimationType.WALK // Use walk animation for dodge - battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.isPlayerHitDelayed -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Player attack on player screen battleSystem.attackPhase == 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen battleSystem.attackPhase == 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen @@ -1161,7 +1229,7 @@ fun PlayerBattleView( } // Calculate hit effect (slight shake) - val hitOffset = if (battleSystem.isPlayerHit) { + val hitOffset = if (battleSystem.isPlayerShakeDelayed) { val shakeAmount = 5.dp val progress = battleSystem.hitProgress // Simple shake effect without complex math @@ -1448,7 +1516,7 @@ fun EnemyBattleView( // Determine animation type based on battle state val animationType = when { battleSystem.isOpponentDodging -> DigimonAnimationType.WALK // Use walk animation for dodge - battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.isOpponentHitDelayed -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on enemy screen else -> DigimonAnimationType.IDLE } @@ -1471,7 +1539,7 @@ fun EnemyBattleView( } // Calculate hit effect (slight shake) - val hitOffset = if (battleSystem.isOpponentHit) { + val hitOffset = if (battleSystem.isOpponentShakeDelayed) { val shakeAmount = 5.dp val progress = battleSystem.hitProgress // Simple shake effect without complex math From f0760f9ed05ca036b219ea85c386f8a07dad00bd Mon Sep 17 00:00:00 2001 From: lightheel Date: Thu, 7 Aug 2025 11:23:32 -0400 Subject: [PATCH 58/89] Added gray boxes around HP --- .../vbhelper/screens/BattlesScreen.kt | 220 +++++++++++------- 1 file changed, 140 insertions(+), 80 deletions(-) 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 e0b808d..ab82952 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 @@ -731,29 +731,44 @@ fun MiddleBattleView( } */ - // Enemy HP bar (top) - LinearProgressIndicator( - progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f), + // Enemy HP bar and text with background box + Box( modifier = Modifier .fillMaxWidth() - .height(10.dp), - color = Color.Red, - trackColor = Color.Gray - ) - - // Enemy HP display numbers - Text( - text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", - fontSize = 16.sp, - color = Color.White, - style = TextStyle( - shadow = Shadow( - color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(4f, 4f), - //blurRadius = 2f + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) ) - ) - ) + .padding(8.dp) + ) { + Column { + // Enemy HP bar (top) + LinearProgressIndicator( + progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", + fontSize = 16.sp, + color = Color.White, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(4f, 4f), + //blurRadius = 2f + ) + ) + ) + } + } Spacer(modifier = Modifier.height(16.dp)) @@ -956,29 +971,44 @@ fun MiddleBattleView( Spacer(modifier = Modifier.height(6.dp)) - // Player HP bar - LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + // Player HP bar and text with background box + Box( modifier = Modifier .fillMaxWidth() - .height(10.dp), - color = Color.Green, - trackColor = Color.Gray - ) - - // Player HP display numbers - Text( - text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, - color = Color.White, - style = TextStyle( - shadow = Shadow( - color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(4f, 4f), - //blurRadius = 2f + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) ) - ) - ) + .padding(8.dp) + ) { + Column { + // Player HP bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Player HP display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 16.sp, + color = Color.White, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(4f, 4f), + //blurRadius = 2f + ) + ) + ) + } + } Spacer(modifier = Modifier.height(6.dp)) @@ -1161,29 +1191,44 @@ fun PlayerBattleView( } */ - // Health bar - LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + // Health bar and text with background box + Box( modifier = Modifier .fillMaxWidth() - .height(10.dp), - color = Color.Green, - trackColor = Color.Gray - ) - - // Health display numbers - Text( - text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, - color = Color.White, - style = TextStyle( - shadow = Shadow( - color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(4f, 4f), - //blurRadius = 2f + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) ) - ) - ) + .padding(8.dp) + ) { + Column { + // Health bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Green, + trackColor = Color.Gray + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Health display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 16.sp, + color = Color.White, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(4f, 4f), + //blurRadius = 2f + ) + ) + ) + } + } } // Middle section: Player Digimon only @@ -1474,29 +1519,44 @@ fun EnemyBattleView( .fillMaxWidth() .padding(16.dp) ) { - // Enemy HP bar - LinearProgressIndicator( - progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + // Enemy HP bar and text with background box + Box( modifier = Modifier .fillMaxWidth() - .height(10.dp), - color = Color.Red, - trackColor = Color.Gray - ) - - // Enemy HP display numbers - Text( - text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, - color = Color.White, - style = TextStyle( - shadow = Shadow( - color = Color.Black, - offset = androidx.compose.ui.geometry.Offset(4f, 4f), - //blurRadius = 2f + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) ) - ) - ) + .padding(8.dp) + ) { + Column { + // Enemy HP bar + LinearProgressIndicator( + progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = Color.Red, + trackColor = Color.Gray + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = 16.sp, + color = Color.White, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = androidx.compose.ui.geometry.Offset(4f, 4f), + //blurRadius = 2f + ) + ) + ) + } + } } // Middle section: Enemy Digimon From bb1c29bbb42ffd6b9dbe4fa41c223f54ac5ba6b8 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 07:44:36 -0400 Subject: [PATCH 59/89] Moved sprites to load from external Android storage. Setup landscape view for battle. --- app/src/main/AndroidManifest.xml | 2 + .../vbhelper/battle/AttackSpriteManager.kt | 19 +- .../vbhelper/battle/BattleSpriteManager.kt | 13 +- .../vbhelper/battle/HitEffectSpriteManager.kt | 44 ++- .../battle/IndividualSpriteManager.kt | 68 ++-- .../vbhelper/battle/SpriteFileManager.kt | 31 +- .../vbhelper/screens/BattlesScreen.kt | 329 ++++++++++-------- 7 files changed, 280 insertions(+), 226 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c28d47b..d8da12c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + () - // Base path for attack textures (updated for new folder structure) - private val attackTexturesPath = "battle_sprites/extracted_atksprites" + // Get the external storage directory for attack sprites + private fun getAttackTexturesPath(): String { + return "VBHelper/battle_sprites/extracted_atksprites" + } fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge") @@ -55,9 +58,10 @@ class AttackSpriteManager(private val context: Context) { return null } - // Load the attack sprite - val attackFilePath = "$attackTexturesPath/$attackFileName.png" - val attackFile = File(context.filesDir, attackFilePath) + // Load the attack sprite from external storage + val externalDir = Environment.getExternalStorageDirectory() + val attackFilePath = "${getAttackTexturesPath()}/$attackFileName.png" + val attackFile = File(externalDir, attackFilePath) println("AttackSpriteManager: Attack file path = ${attackFile.absolutePath}") println("AttackSpriteManager: Attack file exists = ${attackFile.exists()}") @@ -85,8 +89,9 @@ class AttackSpriteManager(private val context: Context) { } try { - // Load character data from JSON file - val characterDataFile = File(context.filesDir, "battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") + // Load character data from JSON file in external storage + val externalDir = Environment.getExternalStorageDirectory() + val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 316d486..f7fd5c1 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.battle import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.os.Environment import com.google.gson.Gson import java.io.File @@ -37,8 +38,11 @@ class BattleSpriteManager(private val context: Context) { private val gson = Gson() private val spriteCache = mutableMapOf() - // Base directory where your sprites are stored - private val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets") + // Get the external storage directory for sprite files + private fun getSpriteBaseDir(): File { + val externalDir = Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_assets") + } fun loadSprite(spriteName: String, atlasName: String): Bitmap? { val cacheKey = "${spriteName}_${atlasName}" @@ -49,6 +53,7 @@ class BattleSpriteManager(private val context: Context) { } // Debug: Check if base directory exists + val spriteBaseDir = getSpriteBaseDir() if (!spriteBaseDir.exists()) { println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}") return null @@ -130,7 +135,7 @@ class BattleSpriteManager(private val context: Context) { // Helper method to get available sprites for an atlas fun getAvailableSprites(atlasName: String): List { try { - val spritesDir = File(spriteBaseDir, "sprites") + val spritesDir = File(getSpriteBaseDir(), "sprites") if (!spritesDir.exists()) { return emptyList() } @@ -154,7 +159,7 @@ class BattleSpriteManager(private val context: Context) { // Helper method to get available atlases fun getAvailableAtlases(): List { try { - val texturesDir = File(spriteBaseDir, "extracted_textures") + val texturesDir = File(getSpriteBaseDir(), "extracted_textures") if (!texturesDir.exists()) { return emptyList() } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt index 33d2189..6536040 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -4,13 +4,17 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Rect +import android.os.Environment import java.io.File class HitEffectSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Base directory where hit effect sprites are stored - private val hitSpritesDir = File(context.filesDir, "battle_sprites/extracted_hit_sprites") + // Get the external storage directory for hit effect sprites + private fun getHitSpritesDir(): File { + val externalDir = Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_hit_sprites") + } /** * Load a hit sprite (hit_01.png, hit_02.png, hit_02_white.png) @@ -26,6 +30,7 @@ class HitEffectSpriteManager(private val context: Context) { } try { + val hitSpritesDir = getHitSpritesDir() val spriteFile = File(hitSpritesDir, "$spriteName.png") if (!spriteFile.exists()) { @@ -68,7 +73,7 @@ class HitEffectSpriteManager(private val context: Context) { } try { - val spritesheetFile = File(hitSpritesDir, "$spritesheetName.png") + val spritesheetFile = File(getHitSpritesDir(), "$spritesheetName.png") if (!spritesheetFile.exists()) { println("Damage effect spritesheet not found: ${spritesheetFile.absolutePath}") @@ -118,28 +123,21 @@ class HitEffectSpriteManager(private val context: Context) { } /** - * Get available hit sprite names - * @return List of available hit sprite names + * Get all available hit sprites + * @return List of hit sprite names (without .png extension) */ fun getAvailableHitSprites(): List { - try { - if (!hitSpritesDir.exists()) { - return emptyList() - } - - val hitFiles = hitSpritesDir.listFiles { file -> - file.name.startsWith("hit_") && file.name.endsWith(".png") - } ?: emptyArray() - - return hitFiles.map { file -> - file.name.substringBefore(".png") - }.sorted() - - } catch (e: Exception) { - println("Error getting available hit sprites: ${e.message}") - e.printStackTrace() + val hitSpritesDir = getHitSpritesDir() + + if (!hitSpritesDir.exists()) { return emptyList() } + + return hitSpritesDir.listFiles { file -> + file.name.startsWith("hit_") && file.name.endsWith(".png") + }?.map { file -> + file.name.substringBefore(".png") + }?.sorted() ?: emptyList() } /** @@ -148,11 +146,11 @@ class HitEffectSpriteManager(private val context: Context) { */ fun getAvailableDamageEffectSpritesheets(): List { try { - if (!hitSpritesDir.exists()) { + if (!getHitSpritesDir().exists()) { return emptyList() } - val dmgFiles = hitSpritesDir.listFiles { file -> + val dmgFiles = getHitSpritesDir().listFiles { file -> file.name.startsWith("dmg_ef") && file.name.endsWith(".png") } ?: emptyArray() diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt index f4b0d25..f6b9179 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt @@ -3,13 +3,17 @@ package com.github.nacabaro.vbhelper.battle import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.os.Environment import java.io.File class IndividualSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Base directory where individual sprite PNGs are stored - private val spriteBaseDir = File(context.filesDir, "battle_sprites/extracted_assets/sprites") + // Get the external storage directory for sprite files + private fun getSpriteBaseDir(): File { + val externalDir = Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_assets/sprites") + } /** * Load a specific sprite frame for a character @@ -26,6 +30,7 @@ class IndividualSpriteManager(private val context: Context) { } // Debug: Check if base directory exists + val spriteBaseDir = getSpriteBaseDir() if (!spriteBaseDir.exists()) { println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}") return null @@ -68,51 +73,38 @@ class IndividualSpriteManager(private val context: Context) { * @return List of frame numbers (1-12) that exist for this character */ fun getAvailableFrames(characterId: String): List { - try { - val characterDir = File(spriteBaseDir, characterId) - if (!characterDir.exists()) { - println("Character directory not found: ${characterDir.absolutePath}") - return emptyList() - } - - val spriteFiles = characterDir.listFiles { file -> - file.name.startsWith("${characterId}_") && file.name.endsWith(".png") - } ?: emptyArray() - - return spriteFiles.mapNotNull { file -> - // Extract frame number from filename (e.g., "dim012_mon03_01.png" -> 1) - val frameNumberStr = file.name.substringAfter("_").substringBefore(".png") - frameNumberStr.toIntOrNull() - }.sorted() - - } catch (e: Exception) { - println("Error getting available frames: ${e.message}") - e.printStackTrace() + val spriteBaseDir = getSpriteBaseDir() + val characterDir = File(spriteBaseDir, characterId) + + if (!characterDir.exists()) { return emptyList() } + + val spriteFiles = characterDir.listFiles { file -> + file.name.startsWith("${characterId}_") && file.name.endsWith(".png") + } ?: emptyArray() + + return spriteFiles.mapNotNull { file -> + val fileName = file.name + val frameMatch = Regex("${characterId}_(\\d{2})\\.png").find(fileName) + frameMatch?.groupValues?.get(1)?.toIntOrNull() + }.sorted() } /** - * Get all available characters + * Get all available character IDs * @return List of character IDs that have sprite directories */ fun getAvailableCharacters(): List { - try { - if (!spriteBaseDir.exists()) { - return emptyList() - } - - val characterDirs = spriteBaseDir.listFiles { file -> - file.isDirectory && file.name.matches(Regex("dim\\d+_mon\\d+.*")) - } ?: emptyArray() - - return characterDirs.map { it.name }.sorted() - - } catch (e: Exception) { - println("Error getting available characters: ${e.message}") - e.printStackTrace() + val spriteBaseDir = getSpriteBaseDir() + + if (!spriteBaseDir.exists()) { return emptyList() } + + return spriteBaseDir.listFiles { file -> + file.isDirectory && file.listFiles()?.any { it.name.endsWith(".png") } == true + }?.map { it.name }?.sorted() ?: emptyList() } /** @@ -128,7 +120,7 @@ class IndividualSpriteManager(private val context: Context) { * @return true if the character has sprite files, false otherwise */ fun hasCharacterSprites(characterId: String): Boolean { - val characterDir = File(spriteBaseDir, characterId) + val characterDir = File(getSpriteBaseDir(), characterId) if (!characterDir.exists()) { return false } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 5dd3699..2f663cb 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -1,15 +1,22 @@ package com.github.nacabaro.vbhelper.battle import android.content.Context +import android.os.Environment import java.io.File import java.io.FileOutputStream import java.io.IOException class SpriteFileManager(private val context: Context) { - fun copySpriteFilesToInternalStorage() { + // Get the external storage directory for sprite files + private fun getSpriteBaseDir(): File { + val externalDir = Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites") + } + + fun copySpriteFilesToExternalStorage() { try { - println("Starting sprite file copy process...") + println("Starting sprite file copy process to external storage...") // Debug: List what's in the assets directory val assetManager = context.assets @@ -47,17 +54,17 @@ class SpriteFileManager(private val context: Context) { } } - // Create the base directory for battle_sprites - val battleSpritesDir = File(context.filesDir, "battle_sprites") + // Create the base directory for battle_sprites in external storage + val battleSpritesDir = getSpriteBaseDir() if (!battleSpritesDir.exists()) { battleSpritesDir.mkdirs() - println("Created battle_sprites directory: ${battleSpritesDir.absolutePath}") + println("Created battle_sprites directory in external storage: ${battleSpritesDir.absolutePath}") } else { - println("battle_sprites directory already exists: ${battleSpritesDir.absolutePath}") + println("battle_sprites directory already exists in external storage: ${battleSpritesDir.absolutePath}") } - // Copy all subdirectories from battle_sprites assets to internal storage - println("Copying all battle_sprites subdirectories...") + // Copy all subdirectories from battle_sprites assets to external storage + println("Copying all battle_sprites subdirectories to external storage...") battleSpritesFiles?.forEach { subdir -> val sourcePath = "battle_sprites/$subdir" val targetDir = File(battleSpritesDir, subdir) @@ -65,7 +72,7 @@ class SpriteFileManager(private val context: Context) { copyAssetDirectory(sourcePath, targetDir) } - println("Sprite files copied successfully to: ${battleSpritesDir.absolutePath}") + println("Sprite files copied successfully to external storage: ${battleSpritesDir.absolutePath}") // Verify that attack sprites were copied val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") @@ -95,7 +102,7 @@ class SpriteFileManager(private val context: Context) { } } catch (e: Exception) { - println("Error copying sprite files: ${e.message}") + println("Error copying sprite files to external storage: ${e.message}") e.printStackTrace() } } @@ -180,7 +187,7 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val battleSpritesDir = File(context.filesDir, "battle_sprites") + val battleSpritesDir = getSpriteBaseDir() val extractedAssetsDir = File(battleSpritesDir, "extracted_assets") val extractedStatsDir = File(battleSpritesDir, "extracted_digimon_stats") val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") @@ -204,7 +211,7 @@ class SpriteFileManager(private val context: Context) { fun clearSpriteFiles() { try { - val battleSpritesDir = File(context.filesDir, "battle_sprites") + val battleSpritesDir = getSpriteBaseDir() if (battleSpritesDir.exists()) { deleteDirectory(battleSpritesDir) 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 ab82952..c9e2f79 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 @@ -66,9 +66,72 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.foundation.Image import androidx.compose.ui.graphics.asImageBitmap import android.graphics.BitmapFactory +import android.os.Environment import java.io.File import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.foundation.layout.width + +@Composable +fun isLandscapeMode(): Boolean { + val configuration = LocalConfiguration.current + return configuration.screenWidthDp > configuration.screenHeightDp +} + +@Composable +fun getLandscapeModifier(): Modifier { + val configuration = LocalConfiguration.current + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + return if (isLandscape) { + Modifier.width(200.dp).height(8.dp) + } else { + Modifier.fillMaxWidth().height(10.dp) + } +} + +@Composable +fun getLandscapeAlignment(): Alignment { + val configuration = LocalConfiguration.current + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + return if (isLandscape) Alignment.Center else Alignment.TopStart +} + +@Composable +fun getLandscapeHorizontalAlignment(): Alignment.Horizontal { + val configuration = LocalConfiguration.current + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + return if (isLandscape) Alignment.CenterHorizontally else Alignment.Start +} + +@Composable +fun getLandscapeFontSize(): androidx.compose.ui.unit.TextUnit { + val configuration = LocalConfiguration.current + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + return if (isLandscape) 14.sp else 16.sp +} + +@Composable +fun getLandscapeBoxModifier(): Modifier { + val configuration = LocalConfiguration.current + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + return if (isLandscape) { + Modifier + .width(220.dp) // Slightly wider than the progress bar to accommodate padding + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) + ) + .padding(8.dp) + } else { + Modifier + .fillMaxWidth() + .background( + color = Color.Gray.copy(alpha = 0.6f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) + ) + .padding(8.dp) + } +} @Composable fun AnimatedDamageNumber( @@ -266,13 +329,13 @@ fun BattleScreen( delay(16) // 60 FPS } println("Phase 2 completed, applying damage and starting Phase 3") - battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) + battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) // Hide damage number and reset pending damage after animation if (showOpponentDamageNumber) { delay(800) // Wait for damage number animation (scale up + hold + fade out) showOpponentDamageNumber = false - pendingOpponentDamage = 0f + pendingOpponentDamage = 0f println("DEBUG: Hiding opponent damage number and resetting pending damage") } @@ -344,18 +407,18 @@ fun BattleScreen( } println("Phase 3 completed, applying damage and resetting") println("DEBUG: pendingPlayerDamage = $pendingPlayerDamage") - battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) // Hide damage number and reset pending damage after animation if (showPlayerDamageNumber) { delay(800) // Wait for damage number animation (scale up + hold + fade out) showPlayerDamageNumber = false - pendingPlayerDamage = 0f + pendingPlayerDamage = 0f println("DEBUG: Hiding player damage number and resetting pending damage") } - battleSystem.resetAttackState() - battleSystem.enableAttackButton() + battleSystem.resetAttackState() + battleSystem.enableAttackButton() // Check if battle is over if (battleSystem.checkBattleOver()) { @@ -733,21 +796,16 @@ fun MiddleBattleView( // Enemy HP bar and text with background box Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Gray.copy(alpha = 0.6f), - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) - ) - .padding(8.dp) + modifier = getLandscapeBoxModifier(), + contentAlignment = getLandscapeAlignment() ) { - Column { + Column( + horizontalAlignment = getLandscapeHorizontalAlignment() + ) { // Enemy HP bar (top) LinearProgressIndicator( progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), + modifier = getLandscapeModifier(), color = Color.Red, trackColor = Color.Gray ) @@ -757,7 +815,7 @@ fun MiddleBattleView( // Enemy HP display numbers Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${opponentCharacter?.baseHp ?: 100}", - fontSize = 16.sp, + fontSize = getLandscapeFontSize(), color = Color.White, style = TextStyle( shadow = Shadow( @@ -962,9 +1020,7 @@ fun MiddleBattleView( // Critical bar LinearProgressIndicator( progress = battleSystem.critBarProgress / 100f, - modifier = Modifier - .fillMaxWidth() - .height(10.dp), + modifier = getLandscapeModifier(), color = Color.Yellow, trackColor = Color.Gray ) @@ -973,21 +1029,16 @@ fun MiddleBattleView( // Player HP bar and text with background box Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Gray.copy(alpha = 0.6f), - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) - ) - .padding(8.dp) + modifier = getLandscapeBoxModifier(), + contentAlignment = getLandscapeAlignment() ) { - Column { + Column( + horizontalAlignment = getLandscapeHorizontalAlignment() + ) { // Player HP bar LinearProgressIndicator( progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), + modifier = getLandscapeModifier(), color = Color.Green, trackColor = Color.Gray ) @@ -997,7 +1048,7 @@ fun MiddleBattleView( // Player HP display numbers Text( text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, + fontSize = getLandscapeFontSize(), color = Color.White, style = TextStyle( shadow = Shadow( @@ -1193,31 +1244,26 @@ fun PlayerBattleView( // Health bar and text with background box Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Gray.copy(alpha = 0.6f), - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) - ) - .padding(8.dp) + modifier = getLandscapeBoxModifier(), + contentAlignment = getLandscapeAlignment() ) { - Column { - // Health bar - LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), - color = Color.Green, - trackColor = Color.Gray - ) + Column( + horizontalAlignment = getLandscapeHorizontalAlignment() + ) { + // Health bar + LinearProgressIndicator( + progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + modifier = getLandscapeModifier(), + color = Color.Green, + trackColor = Color.Gray + ) Spacer(modifier = Modifier.height(4.dp)) - // Health display numbers - Text( - text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, + // Health display numbers + Text( + text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + fontSize = getLandscapeFontSize(), color = Color.White, style = TextStyle( shadow = Shadow( @@ -1363,9 +1409,7 @@ fun PlayerBattleView( // Critical bar LinearProgressIndicator( progress = battleSystem.critBarProgress / 100f, - modifier = Modifier - .fillMaxWidth() - .height(10.dp), + modifier = getLandscapeModifier(), color = Color.Yellow, trackColor = Color.Gray ) @@ -1422,22 +1466,22 @@ fun PlayerBattleView( // Match is still ongoing - update HP and continue println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Set pending damage based on API result + // Set pending damage based on API result if (apiResult.playerAttackDamage > 0) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) // Also check if enemy counter-attacks and hits if (apiResult.opponentAttackDamage > 0) { println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage } - } else { + } else { // Player attack missed - enemy counter-attacks println("Player attack missed! Enemy counter-attacks") - battleSystem.setAttackHitState(false) + battleSystem.setAttackHitState(false) // Set up counter-attack - determine if it hits based on API result val counterAttackHits = apiResult.opponentAttackDamage > 0 println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") @@ -1458,7 +1502,7 @@ fun PlayerBattleView( battleSystem.setupCounterAttack(finalCounterAttackHits) // Set the opponent attack hit state for Phase 3 battleSystem.handleOpponentAttackResult(finalCounterAttackHits) - } + } } 2 -> { // Match is over - transition to results screen @@ -1514,28 +1558,23 @@ fun EnemyBattleView( MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) // Top section: Enemy HP bar and HP numbers - Column( + Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { // Enemy HP bar and text with background box Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Gray.copy(alpha = 0.6f), - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) - ) - .padding(8.dp) + modifier = getLandscapeBoxModifier(), + contentAlignment = getLandscapeAlignment() ) { - Column { + Column( + horizontalAlignment = getLandscapeHorizontalAlignment() + ) { // Enemy HP bar LinearProgressIndicator( progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), - modifier = Modifier - .fillMaxWidth() - .height(10.dp), + modifier = getLandscapeModifier(), color = Color.Red, trackColor = Color.Gray ) @@ -1545,7 +1584,7 @@ fun EnemyBattleView( // Enemy HP display numbers Text( text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 16.sp, + fontSize = getLandscapeFontSize(), color = Color.White, style = TextStyle( shadow = Shadow( @@ -1561,25 +1600,25 @@ fun EnemyBattleView( // Middle section: Enemy Digimon Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp), contentAlignment = Alignment.Center - ) { + ) { // Enemy Digimon - Box( - modifier = Modifier - .fillMaxWidth() - .size(80.dp), - contentAlignment = Alignment.CenterEnd - ) { - // Determine animation type based on battle state + Box( + modifier = Modifier + .fillMaxWidth() + .size(80.dp), + contentAlignment = Alignment.CenterEnd + ) { + // Determine animation type based on battle state val animationType = when { battleSystem.isOpponentDodging -> DigimonAnimationType.WALK // Use walk animation for dodge battleSystem.isOpponentHitDelayed -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on enemy screen - else -> DigimonAnimationType.IDLE - } + else -> DigimonAnimationType.IDLE + } // Calculate vertical offset for dodge animation val verticalOffset = if (battleSystem.isOpponentDodging) { @@ -1608,65 +1647,65 @@ fun EnemyBattleView( } else { 0.dp } - - AnimatedSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - animationType = animationType, + + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = animationType, modifier = Modifier .size(80.dp) .offset( x = hitOffset, y = verticalOffset ), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit, reloadMappings = false, animationOffset = 375L // Offset enemy animation by half the idle duration - ) - - // Attack sprite visibility and positioning based on attack phase - val shouldShowAttack = when (battleSystem.attackPhase) { + ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { 2 -> true // Player attack on enemy screen - else -> false - } - - if (shouldShowAttack) { + else -> false + } + + if (shouldShowAttack) { val xOffset = (attackAnimationProgress * 400 - 350).dp // Player attack on enemy screen - start more to the left // Use player's character ID for player attack val characterId = playerCharacter?.charaId ?: "dim011_mon01" - - // Handle sprite transition - LaunchedEffect(characterId, battleSystem.attackPhase) { - if ((previousCharacterId != null && previousCharacterId != characterId) || - (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { - // Character ID or attack phase changed, start transition - isTransitioning = true - delay(100) // Brief invisibility period - isTransitioning = false - } - previousCharacterId = characterId - previousAttackPhase = battleSystem.attackPhase + + // Handle sprite transition + LaunchedEffect(characterId, battleSystem.attackPhase) { + if ((previousCharacterId != null && previousCharacterId != characterId) || + (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { + // Character ID or attack phase changed, start transition + isTransitioning = true + delay(100) // Brief invisibility period + isTransitioning = false } - + previousCharacterId = characterId + previousAttackPhase = battleSystem.attackPhase + } + println("EnemyBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - + if (!isTransitioning && !hideEnemyAttackSprite) { - AttackSpriteImage( - characterId = characterId, - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) + AttackSpriteImage( + characterId = characterId, + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) .scale(-1f, 1f), // Flip player attacks - contentScale = ContentScale.Fit - ) - } + contentScale = ContentScale.Fit + ) } } } + } } } @@ -1735,10 +1774,10 @@ fun BattlesScreen() { println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") val spriteFileManager = SpriteFileManager(context) if (!spriteFileManager.checkSpriteFilesExist()) { - println("BATTLESCREEN: Copying sprite files to internal storage...") - spriteFileManager.copySpriteFilesToInternalStorage() + println("BATTLESCREEN: Copying sprite files to external storage...") + spriteFileManager.copySpriteFilesToExternalStorage() } else { - println("BATTLESCREEN: Sprite files already exist in internal storage") + println("BATTLESCREEN: Sprite files already exist in external storage") } } @@ -2048,9 +2087,9 @@ fun BattlesScreen() { topBar = { // Only show TopBanner when not in battle mode if (currentView != "battle-main" && currentView != "battle-results") { - TopBanner( - text = "Online battles" - ) + TopBanner( + text = "Online battles" + ) } } ) { contentPadding -> @@ -2077,6 +2116,7 @@ fun BattlesScreen() { championButton() ultimateButton() megaButton() + /* Button( onClick = { showSpriteTester = true @@ -2098,6 +2138,8 @@ fun BattlesScreen() { ) { Text("Clear Sprite Files") } + + */ } } } @@ -2387,10 +2429,11 @@ fun AnimatedBattleBackground( println("DEBUG: Screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load background image from internal storage + // Load background image from external storage LaunchedEffect(Unit) { try { - val backgroundFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + val externalDir = Environment.getExternalStorageDirectory() + val backgroundFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") if (backgroundFile.exists()) { backgroundBitmap = BitmapFactory.decodeFile(backgroundFile.absolutePath) println("Successfully loaded battle background: ${backgroundFile.absolutePath}") @@ -2469,11 +2512,13 @@ fun MultiLayerAnimatedBattleBackground( println("DEBUG: Multi-layer screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load all three background layers from internal storage + // Load all three background layers from external storage LaunchedEffect(Unit) { try { + val externalDir = Environment.getExternalStorageDirectory() + // Back layer (BattleBg_0018_BattleBg_0013.png) - val backLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0018_BattleBg_0013.png") + val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0018_BattleBg_0013.png") if (backLayerFile.exists()) { backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) println("Successfully loaded back layer background: ${backLayerFile.absolutePath}") @@ -2482,7 +2527,7 @@ fun MultiLayerAnimatedBattleBackground( } // Middle layer (BattleBg_0015_BattleBg_0012.png) - val middleLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") if (middleLayerFile.exists()) { middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) println("Successfully loaded middle layer background: ${middleLayerFile.absolutePath}") @@ -2491,7 +2536,7 @@ fun MultiLayerAnimatedBattleBackground( } // Front layer (BattleBg_0005_BattleBg_0011.png) - val frontLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0005_BattleBg_0011.png") + val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0005_BattleBg_0011.png") if (frontLayerFile.exists()) { frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) println("Successfully loaded front layer background: ${frontLayerFile.absolutePath}") From ba03be808ed13183809a6ca2d0bf7dcea3bc8f8b Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 07:51:51 -0400 Subject: [PATCH 60/89] Removed attack bar and button from player view. --- .../vbhelper/screens/BattlesScreen.kt | 134 ------------------ 1 file changed, 134 deletions(-) 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 c9e2f79..6591404 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 @@ -1398,141 +1398,7 @@ fun PlayerBattleView( } } - // Bottom section: Critical bar and Attack button - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .align(Alignment.BottomCenter), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Critical bar - LinearProgressIndicator( - progress = battleSystem.critBarProgress / 100f, - modifier = getLandscapeModifier(), - color = Color.Yellow, - trackColor = Color.Gray - ) - Spacer(modifier = Modifier.height(16.dp)) - - // Attack button - Button( - onClick = { - println("Attack button clicked!") - - // Get crit bar progress as float (0.0f to 100.0f) - val critBarProgressFloat = battleSystem.critBarProgress.toFloat() - - // Determine player and opponent stages - val playerStage = when (activeCharacter?.stage) { - 0 -> 0 // rookie - 1 -> 1 // champion - 2 -> 2 // ultimate - 3 -> 3 // mega - else -> 0 - } - - val opponentStage = when (opponent?.stage) { - 0 -> 0 // rookie - 1 -> 1 // champion - 2 -> 2 // ultimate - 3 -> 3 // mega - else -> 0 - } - - // Send API call with all parameters - context?.let { ctx -> - // Start player attack animation - battleSystem.startPlayerAttack() - - RetrofitHelper().getPVPWinner( - ctx, - 1, - 2, - activeCharacter?.name ?: "Player", - playerStage, - opponentStage, - opponent?.name ?: "Opponent", - opponentStage - ) { apiResult -> - // Handle API response here - println("API Result: $apiResult") - lastApiResult = apiResult // Store for debug display - - // Update HP based on API response - when (apiResult.state) { - 1 -> { - // Match is still ongoing - update HP and continue - println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - - // Set pending damage based on API result - if (apiResult.playerAttackDamage > 0) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) - - // Also check if enemy counter-attacks and hits - if (apiResult.opponentAttackDamage > 0) { - println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage - } - } else { - // Player attack missed - enemy counter-attacks - println("Player attack missed! Enemy counter-attacks") - battleSystem.setAttackHitState(false) - // Set up counter-attack - determine if it hits based on API result - val counterAttackHits = apiResult.opponentAttackDamage > 0 - println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") - println("Full API response: status=${apiResult.status}, state=${apiResult.state}, playerAttackHit=${apiResult.playerAttackHit}, playerAttackDamage=${apiResult.playerAttackDamage}, opponentAttackDamage=${apiResult.opponentAttackDamage}, playerHP=${apiResult.playerHP}, opponentHP=${apiResult.opponentHP}") - println("DEBUG: Using playerAttackDamage > 0 instead of playerAttackHit for hit detection") - - // Use opponentAttackDamage to determine counter-attack hit - val finalCounterAttackHits = counterAttackHits - println("Using opponentAttackDamage > 0 for counter-attack: $finalCounterAttackHits") - - if (finalCounterAttackHits) { - println("Counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage - } else { - println("Counter-attack misses! Player dodges") - onSetPendingDamage(0f, 0f) // No damage - } - battleSystem.setupCounterAttack(finalCounterAttackHits) - // Set the opponent attack hit state for Phase 3 - battleSystem.handleOpponentAttackResult(finalCounterAttackHits) - } - } - 2 -> { - // Match is over - transition to results screen - println("Match is over! Winner: ${apiResult.winner}") - battleSystem.updateHPFromAPI(apiResult.playerHP.toFloat(), apiResult.opponentHP.toFloat()) - onAttackClick() // This will transition to battle-results screen - } - -1 -> { - // Error occurred - println("API Error: ${apiResult.status}") - battleSystem.resetAttackState() - battleSystem.enableAttackButton() - } - } - } - } - }, - enabled = battleSystem.isAttackButtonEnabled, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Red, - disabledContainerColor = Color.Gray - ), - shape = RoundedCornerShape(8.dp) - ) { - Text("Attack", color = Color.White, fontSize = 18.sp) - } - } } } From 1bbaf66c242f73f29347e1e082279bd545f154ae Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 09:24:30 -0400 Subject: [PATCH 61/89] Removed exit button from view until battle is over. --- .../vbhelper/screens/BattlesScreen.kt | 61 +------------------ 1 file changed, 3 insertions(+), 58 deletions(-) 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 6591404..d2c2c1c 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 @@ -761,39 +761,12 @@ fun MiddleBattleView( modifier = Modifier.fillMaxSize() ) - // Top section: Exit button, HP bars, and HP numbers + // Top section: HP bars and HP numbers Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { - // Exit button at the top-right - Box( - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = { /* TODO: Add exit functionality */ }, - modifier = Modifier.align(Alignment.TopEnd), - colors = ButtonDefaults.buttonColors(containerColor = Color.Red) - ) { - Text("Exit", color = Color.White, fontSize = 14.sp) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Debug display - /* - if (lastApiResult != null) { - Text( - text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", - color = Color.Red, - fontSize = 10.sp - ) - Spacer(modifier = Modifier.height(8.dp)) - } - */ - // Enemy HP bar and text with background box Box( modifier = getLandscapeBoxModifier(), @@ -1209,39 +1182,12 @@ fun PlayerBattleView( // Multi-layer animated battle background MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) - // Top section: Exit button, HP bar, and HP numbers + // Top section: HP bar and HP numbers Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { - // Exit button at the top-right - Box( - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = { /* TODO: Add exit functionality */ }, - modifier = Modifier.align(Alignment.TopEnd), - colors = ButtonDefaults.buttonColors(containerColor = Color.Red) - ) { - Text("Exit", color = Color.White, fontSize = 12.sp) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Debug display - /* - if (lastApiResult != null) { - Text( - text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", - color = Color.Red, - fontSize = 10.sp - ) - Spacer(modifier = Modifier.height(8.dp)) - } - */ - // Health bar and text with background box Box( modifier = getLandscapeBoxModifier(), @@ -1995,6 +1941,7 @@ fun BattlesScreen() { ) { Text("Sprite Animation Tester") } + */ Button( onClick = { val spriteFileManager = SpriteFileManager(context) @@ -2004,8 +1951,6 @@ fun BattlesScreen() { ) { Text("Clear Sprite Files") } - - */ } } } From 6a6369ae9eccd93efe229dd11ee3d45bbb745a48 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 12:17:47 -0400 Subject: [PATCH 62/89] Adjusted attack hit sprite location in landscape view. --- .../vbhelper/battle/HitEffectComposables.kt | 19 +++++++++++++++++-- .../vbhelper/screens/BattlesScreen.kt | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt index 5110d1c..1f3210e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -25,6 +26,8 @@ fun HitEffectOverlay( if (!isVisible) return val context = LocalContext.current + val configuration = LocalConfiguration.current + val isLandscapeMode = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE val hitEffectManager = remember { HitEffectSpriteManager(context) } val coroutineScope = rememberCoroutineScope() @@ -92,10 +95,22 @@ fun HitEffectOverlay( .offset( x = if (isPlayerScreen) { // On player screen, position further to the left - (-sprite.width * scale / 2 - 100).dp + if (isLandscapeMode) { + // In landscape mode, move even further left for player screen + (-sprite.width * scale / 2 - 300).dp + } else { + // In portrait mode, use original positioning + (-sprite.width * scale / 2 - 100).dp + } } else { // On enemy screen, position further to the right - (-sprite.width * scale / 2 + 150).dp + if (isLandscapeMode) { + // In landscape mode, move even further right for enemy screen + (-sprite.width * scale / 2 + 350).dp + } else { + // In portrait mode, use original positioning + (-sprite.width * scale / 2 + 150).dp + } }, y = (-sprite.height * scale / 2 + 40).dp // Position lower on screen (was -60, now +40) ), 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 d2c2c1c..373b8eb 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 @@ -1941,7 +1941,6 @@ fun BattlesScreen() { ) { Text("Sprite Animation Tester") } - */ Button( onClick = { val spriteFileManager = SpriteFileManager(context) @@ -1951,6 +1950,7 @@ fun BattlesScreen() { ) { Text("Clear Sprite Files") } + */ } } } From 16cd7abce8cc306cd708d9814c5b8acd19a7a2fe Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 12:37:32 -0400 Subject: [PATCH 63/89] Added multiple background sets for battle. --- .../vbhelper/screens/BattlesScreen.kt | 92 ++++++++++++++----- 1 file changed, 70 insertions(+), 22 deletions(-) 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 373b8eb..8812e0f 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 @@ -204,7 +204,8 @@ fun BattleScreen( activeCharacter: APIBattleCharacter?, opponentCharacter: APIBattleCharacter?, onAttackClick: () -> Unit, - context: android.content.Context? = null + context: android.content.Context? = null, + selectedBackgroundSet: Int = 0 ) { val battleSystem = remember { ArenaBattleSystem() } val coroutineScope = rememberCoroutineScope() @@ -617,7 +618,8 @@ fun BattleScreen( }, coroutineScope = coroutineScope, hidePlayerAttackSprite = hidePlayerAttackSprite, - hideEnemyAttackSprite = hideEnemyAttackSprite + hideEnemyAttackSprite = hideEnemyAttackSprite, + selectedBackgroundSet = selectedBackgroundSet ) } 1 -> { @@ -638,7 +640,8 @@ fun BattleScreen( pendingOpponentDamage = opponentDamage }, coroutineScope = coroutineScope, - hidePlayerAttackSprite = hidePlayerAttackSprite + hidePlayerAttackSprite = hidePlayerAttackSprite, + selectedBackgroundSet = selectedBackgroundSet ) } 2 -> { @@ -650,7 +653,8 @@ fun BattleScreen( attackAnimationProgress = battleSystem.attackProgress, activeCharacter = opponentCharacter, playerCharacter = activeCharacter, - hideEnemyAttackSprite = hideEnemyAttackSprite + hideEnemyAttackSprite = hideEnemyAttackSprite, + selectedBackgroundSet = selectedBackgroundSet ) } } @@ -745,7 +749,8 @@ fun MiddleBattleView( onSetPendingDamage: (Float, Float) -> Unit, coroutineScope: kotlinx.coroutines.CoroutineScope, hidePlayerAttackSprite: Boolean, - hideEnemyAttackSprite: Boolean + hideEnemyAttackSprite: Boolean, + selectedBackgroundSet: Int = 0 ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -758,7 +763,8 @@ fun MiddleBattleView( ) { // Animated background - positioned underneath all other sprites MultiLayerAnimatedBattleBackground( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + backgroundSetIndex = selectedBackgroundSet ) // Top section: HP bars and HP numbers @@ -1168,7 +1174,8 @@ fun PlayerBattleView( opponent: APIBattleCharacter?, onSetPendingDamage: (Float, Float) -> Unit, coroutineScope: kotlinx.coroutines.CoroutineScope, - hidePlayerAttackSprite: Boolean + hidePlayerAttackSprite: Boolean, + selectedBackgroundSet: Int = 0 ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -1180,7 +1187,7 @@ fun PlayerBattleView( modifier = Modifier.fillMaxSize() ) { // Multi-layer animated battle background - MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize(), backgroundSetIndex = selectedBackgroundSet) // Top section: HP bar and HP numbers Column( @@ -1356,7 +1363,8 @@ fun EnemyBattleView( attackAnimationProgress: Float, activeCharacter: APIBattleCharacter? = null, playerCharacter: APIBattleCharacter? = null, - hideEnemyAttackSprite: Boolean + hideEnemyAttackSprite: Boolean, + selectedBackgroundSet: Int = 0 ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } @@ -1367,7 +1375,7 @@ fun EnemyBattleView( modifier = Modifier.fillMaxSize() ) { // Multi-layer animated battle background - MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize()) + MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize(), backgroundSetIndex = selectedBackgroundSet) // Top section: Enemy HP bar and HP numbers Column( @@ -1537,6 +1545,9 @@ fun BattlesScreen() { var selectedStage by remember { mutableStateOf("") } var currentStage by remember { mutableStateOf("rookie") } + // Random background set selection + var selectedBackgroundSet by remember { mutableStateOf(0) } + // Sprite animation tester state var showSpriteTester by remember { mutableStateOf(false) } var spriteTesterView by remember { mutableStateOf("entry") } // "entry" or "testing" @@ -1970,6 +1981,8 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> currentView = "battle-main" } @@ -2008,6 +2021,8 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> currentView = "battle-main" } @@ -2046,6 +2061,8 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> currentView = "battle-main" } @@ -2084,6 +2101,8 @@ fun BattlesScreen() { onClick = { activeCharacter?.let { selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> currentView = "battle-main" } @@ -2118,7 +2137,8 @@ fun BattlesScreen() { // This will be called when the battle is over currentView = "battle-results" }, - context = context + context = context, + selectedBackgroundSet = selectedBackgroundSet ) } @@ -2298,9 +2318,36 @@ fun AnimatedBattleBackground( } } +// Data class to define background sets +data class BackgroundSet( + val backLayer: String, + val middleLayer: String, + val frontLayer: String +) + +// Define the three background sets +val backgroundSets = listOf( + BackgroundSet( + backLayer = "BattleBg_0018_BattleBg_0013.png", + middleLayer = "BattleBg_0015_BattleBg_0012.png", + frontLayer = "BattleBg_0005_BattleBg_0011.png" + ), + BackgroundSet( + backLayer = "BattleBg_0014_BattleBg_0013.png", + middleLayer = "BattleBg_0010_BattleBg_0012.png", + frontLayer = "BattleBg_0011_BattleBg_0011.png" + ), + BackgroundSet( + backLayer = "BattleBg_0019_BattleBg_0013.png", + middleLayer = "BattleBg_0004_BattleBg_0012.png", + frontLayer = "BattleBg_0009_BattleBg_0011.png" + ) +) + @Composable fun MultiLayerAnimatedBattleBackground( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + backgroundSetIndex: Int = 0 ) { val context = LocalContext.current var backLayerBitmap by remember { mutableStateOf(null) } @@ -2324,33 +2371,34 @@ fun MultiLayerAnimatedBattleBackground( } // Load all three background layers from external storage - LaunchedEffect(Unit) { + LaunchedEffect(backgroundSetIndex) { try { val externalDir = Environment.getExternalStorageDirectory() + val selectedSet = backgroundSets[backgroundSetIndex] - // Back layer (BattleBg_0018_BattleBg_0013.png) - val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0018_BattleBg_0013.png") + // Back layer + val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") if (backLayerFile.exists()) { backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) - println("Successfully loaded back layer background: ${backLayerFile.absolutePath}") + println("Successfully loaded back layer background (Set ${backgroundSetIndex + 1}): ${backLayerFile.absolutePath}") } else { println("Back layer background file not found: ${backLayerFile.absolutePath}") } - // Middle layer (BattleBg_0015_BattleBg_0012.png) - val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + // Middle layer + val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") if (middleLayerFile.exists()) { middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) - println("Successfully loaded middle layer background: ${middleLayerFile.absolutePath}") + println("Successfully loaded middle layer background (Set ${backgroundSetIndex + 1}): ${middleLayerFile.absolutePath}") } else { println("Middle layer background file not found: ${middleLayerFile.absolutePath}") } - // Front layer (BattleBg_0005_BattleBg_0011.png) - val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0005_BattleBg_0011.png") + // Front layer + val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") if (frontLayerFile.exists()) { frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) - println("Successfully loaded front layer background: ${frontLayerFile.absolutePath}") + println("Successfully loaded front layer background (Set ${backgroundSetIndex + 1}): ${frontLayerFile.absolutePath}") } else { println("Front layer background file not found: ${frontLayerFile.absolutePath}") } From 5ed7d117f557b8472340e98b845f4f71e92890a2 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 8 Aug 2025 13:20:40 -0400 Subject: [PATCH 64/89] Fixed background scaling issues in landscape view. --- .../vbhelper/screens/BattlesScreen.kt | 148 +++++++++--------- 1 file changed, 71 insertions(+), 77 deletions(-) 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 8812e0f..6c0407f 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 @@ -903,13 +903,18 @@ fun MiddleBattleView( } } - // Horizontal line separator - Box( - modifier = Modifier - .fillMaxWidth() - .height(2.dp) - .background(Color.Black) - ) + // Horizontal line separator (hidden in landscape mode) + val lineConfiguration = LocalConfiguration.current + val isLandscapeMode = lineConfiguration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE + + if (!isLandscapeMode) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background(Color.Black) + ) + } // Player Digimon (bottom half) Box( @@ -2295,25 +2300,22 @@ fun AnimatedBattleBackground( backgroundBitmap?.let { bitmap -> Box(modifier = modifier.fillMaxSize()) { - // First image (main) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Animated Battle Background 1", - modifier = Modifier - .fillMaxSize() - .offset(x = xOffset.dp), - contentScale = ContentScale.FillBounds - ) + // Calculate how many times to repeat the image to fill the screen width + val configuration = LocalConfiguration.current + val isLandscapeMode = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE + val imageWidth = if (isLandscapeMode) screenWidth.value * 1.5f else screenWidth.value + val repeatCount = (imageWidth / screenWidth.value).toInt() + 2 // Add 2 for seamless looping - // Second image (for seamless loop) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Animated Battle Background 2", - modifier = Modifier - .fillMaxSize() - .offset(x = (xOffset + screenWidth.value).dp), - contentScale = ContentScale.FillBounds - ) + repeat(repeatCount) { index -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Animated Battle Background ${index + 1}", + modifier = Modifier + .fillMaxSize() + .offset(x = (xOffset + (index * screenWidth.value)).dp), + contentScale = ContentScale.FillBounds + ) + } } } } @@ -2407,26 +2409,32 @@ fun MultiLayerAnimatedBattleBackground( } } - // Animate all three layers with different speeds + // Animate all three layers with different speeds (slower in landscape mode) + val bgConfiguration = LocalConfiguration.current + val isLandscapeMode = bgConfiguration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE + LaunchedEffect(screenWidth) { if (screenWidth > 0.dp) { while (true) { delay(50) // Update every 50ms for smooth animation + // Adjust speed based on orientation + val speedMultiplier = if (isLandscapeMode) 0.5f else 1f + // Back layer moves slowest (parallax effect) - backLayerXOffset -= 0.5f + backLayerXOffset -= 0.5f * speedMultiplier if (backLayerXOffset <= -screenWidth.value) { backLayerXOffset = 0f } // Middle layer moves at medium speed - middleLayerXOffset -= 1f + middleLayerXOffset -= 1f * speedMultiplier if (middleLayerXOffset <= -screenWidth.value) { middleLayerXOffset = 0f } // Front layer moves fastest - frontLayerXOffset -= 1.5f + frontLayerXOffset -= 1.5f * speedMultiplier if (frontLayerXOffset <= -screenWidth.value) { frontLayerXOffset = 0f } @@ -2435,64 +2443,50 @@ fun MultiLayerAnimatedBattleBackground( } Box(modifier = modifier.fillMaxSize()) { + // Calculate how many times to repeat the image to fill the screen width + val imageWidth = if (isLandscapeMode) screenWidth.value * 1.5f else screenWidth.value + val repeatCount = (imageWidth / screenWidth.value).toInt() + 2 // Add 2 for seamless looping + // Back layer (underneath everything) backLayerBitmap?.let { bitmap -> - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Back Layer Battle Background 1", - modifier = Modifier - .fillMaxSize() - .offset(x = backLayerXOffset.dp), - contentScale = ContentScale.FillBounds - ) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Back Layer Battle Background 2", - modifier = Modifier - .fillMaxSize() - .offset(x = (backLayerXOffset + screenWidth.value).dp), - contentScale = ContentScale.FillBounds - ) + repeat(repeatCount) { index -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Back Layer Battle Background ${index + 1}", + modifier = Modifier + .fillMaxSize() + .offset(x = (backLayerXOffset + (index * screenWidth.value)).dp), + contentScale = ContentScale.FillBounds + ) + } } // Middle layer middleLayerBitmap?.let { bitmap -> - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Middle Layer Battle Background 1", - modifier = Modifier - .fillMaxSize() - .offset(x = middleLayerXOffset.dp), - contentScale = ContentScale.FillBounds - ) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Middle Layer Battle Background 2", - modifier = Modifier - .fillMaxSize() - .offset(x = (middleLayerXOffset + screenWidth.value).dp), - contentScale = ContentScale.FillBounds - ) + repeat(repeatCount) { index -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Middle Layer Battle Background ${index + 1}", + modifier = Modifier + .fillMaxSize() + .offset(x = (middleLayerXOffset + (index * screenWidth.value)).dp), + contentScale = ContentScale.FillBounds + ) + } } // Front layer (on top of other backgrounds) frontLayerBitmap?.let { bitmap -> - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Front Layer Battle Background 1", - modifier = Modifier - .fillMaxSize() - .offset(x = frontLayerXOffset.dp), - contentScale = ContentScale.FillBounds - ) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = "Front Layer Battle Background 2", - modifier = Modifier - .fillMaxSize() - .offset(x = (frontLayerXOffset + screenWidth.value).dp), - contentScale = ContentScale.FillBounds - ) + repeat(repeatCount) { index -> + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Front Layer Battle Background ${index + 1}", + modifier = Modifier + .fillMaxSize() + .offset(x = (frontLayerXOffset + (index * screenWidth.value)).dp), + contentScale = ContentScale.FillBounds + ) + } } } } \ No newline at end of file From c122b71b4612d923bcff7ba7cfe1208a619c4410 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 15 Aug 2025 11:55:45 -0400 Subject: [PATCH 65/89] Changed references to be correct. --- .../java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6c0407f..11c39a6 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 @@ -1576,7 +1576,7 @@ fun BattlesScreen() { val ultimateCharacters = listOf( APIBattleCharacter("METALGREYMON (VIRUS)","degimon_name_Dim014_005","dim014_mon05",2,2,2640, 2640, 2450.0f,800.0f), - APIBattleCharacter("MAMEMON", "degimon_name_Dim000_005", "dim000_mon05", 2, 1, 3000, 3000, 4000.0f, 1000.0f), + APIBattleCharacter("MAMEMON", "degimon_name_Dim012_011", "dim012_mon11", 2, 1, 3000, 3000, 4000.0f, 1000.0f), APIBattleCharacter("DORUGREYMON","degimon_name_dim137_mon09","dim137_mon09",2,3,5000, 5000, 6400.0f,1400.0f) ) From 68ad57b78f2cb9a7cf82f3e167bf35422f53fb4f Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 08:24:50 -0400 Subject: [PATCH 66/89] Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ff3a42..78e9b77 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex app/src/main/java/com/github/nacabaro/vbhelper/battle/Battle_Sprites_Reference/ -app/src/main/assets/battle_sprites \ No newline at end of file +app/src/main/assets/battle_sprites + +API-ACR122USAM-2.01.pdf \ No newline at end of file From ac02205f762917cacb9423de8f775357d9eb91ec Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 10:20:01 -0400 Subject: [PATCH 67/89] Updated sprites to load from phone's internal storage. --- .../vbhelper/battle/AttackSpriteManager.kt | 14 ++- .../vbhelper/battle/BattleSpriteManager.kt | 5 +- .../vbhelper/battle/HitEffectSpriteManager.kt | 5 +- .../battle/IndividualSpriteManager.kt | 5 +- .../vbhelper/battle/SpriteFileManager.kt | 102 ++++++++++++++++-- .../vbhelper/screens/BattlesScreen.kt | 20 ++-- 6 files changed, 114 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt index 4f7d1ec..2a03ea8 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -32,9 +32,9 @@ class AttackSpriteManager(private val context: Context) { private val gson = Gson() private val characterDataCache = mutableMapOf() - // Get the external storage directory for attack sprites + // Get the internal storage directory for attack sprites private fun getAttackTexturesPath(): String { - return "VBHelper/battle_sprites/extracted_atksprites" + return "battle_sprites/extracted_atksprites" } fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { @@ -58,10 +58,9 @@ class AttackSpriteManager(private val context: Context) { return null } - // Load the attack sprite from external storage - val externalDir = Environment.getExternalStorageDirectory() + // Load the attack sprite from internal storage val attackFilePath = "${getAttackTexturesPath()}/$attackFileName.png" - val attackFile = File(externalDir, attackFilePath) + val attackFile = File(context.filesDir, attackFilePath) println("AttackSpriteManager: Attack file path = ${attackFile.absolutePath}") println("AttackSpriteManager: Attack file exists = ${attackFile.exists()}") @@ -89,9 +88,8 @@ class AttackSpriteManager(private val context: Context) { } try { - // Load character data from JSON file in external storage - val externalDir = Environment.getExternalStorageDirectory() - val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") + // Load character data from JSON file in internal storage + val characterDataFile = File(context.filesDir, "battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index f7fd5c1..7f1708b 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -38,10 +38,9 @@ class BattleSpriteManager(private val context: Context) { private val gson = Gson() private val spriteCache = mutableMapOf() - // Get the external storage directory for sprite files + // Get the internal storage directory for sprite files private fun getSpriteBaseDir(): File { - val externalDir = Environment.getExternalStorageDirectory() - return File(externalDir, "VBHelper/battle_sprites/extracted_assets") + return File(context.filesDir, "battle_sprites/extracted_assets") } fun loadSprite(spriteName: String, atlasName: String): Bitmap? { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt index 6536040..b7fabfa 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -10,10 +10,9 @@ import java.io.File class HitEffectSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Get the external storage directory for hit effect sprites + // Get the internal storage directory for hit effect sprites private fun getHitSpritesDir(): File { - val externalDir = Environment.getExternalStorageDirectory() - return File(externalDir, "VBHelper/battle_sprites/extracted_hit_sprites") + return File(context.filesDir, "battle_sprites/extracted_hit_sprites") } /** diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt index f6b9179..09a87e7 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt @@ -9,10 +9,9 @@ import java.io.File class IndividualSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Get the external storage directory for sprite files + // Get the internal storage directory for sprite files private fun getSpriteBaseDir(): File { - val externalDir = Environment.getExternalStorageDirectory() - return File(externalDir, "VBHelper/battle_sprites/extracted_assets/sprites") + return File(context.filesDir, "battle_sprites/extracted_assets/sprites") } /** diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 2f663cb..1baa2cd 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -1,19 +1,70 @@ package com.github.nacabaro.vbhelper.battle import android.content.Context -import android.os.Environment import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.FileInputStream class SpriteFileManager(private val context: Context) { - // Get the external storage directory for sprite files - private fun getSpriteBaseDir(): File { - val externalDir = Environment.getExternalStorageDirectory() + // Get the external storage directory where files are already located + private fun getExternalSpriteBaseDir(): File { + val externalDir = android.os.Environment.getExternalStorageDirectory() return File(externalDir, "VBHelper/battle_sprites") } + // Get the internal storage directory for sprite files + private fun getInternalSpriteBaseDir(): File { + return File(context.filesDir, "battle_sprites") + } + + fun copySpriteFilesToInternalStorage() { + try { + println("Starting sprite file copy process from external storage to internal storage...") + + val externalDir = getExternalSpriteBaseDir() + val internalDir = getInternalSpriteBaseDir() + + // Check if external directory exists + if (!externalDir.exists()) { + println("External sprite directory does not exist: ${externalDir.absolutePath}") + return + } + + println("External sprite directory exists: ${externalDir.absolutePath}") + println("Copying to internal storage: ${internalDir.absolutePath}") + + // Create internal directory if it doesn't exist + if (!internalDir.exists()) { + val created = internalDir.mkdirs() + println("Created internal sprite directory: $created") + } + + // Copy all subdirectories from external to internal storage + val externalFiles = externalDir.listFiles() + if (externalFiles != null) { + println("Found ${externalFiles.size} items in external directory") + externalFiles.forEach { item -> + val targetItem = File(internalDir, item.name) + if (item.isDirectory) { + println("Copying directory: ${item.name}") + copyDirectory(item, targetItem) + } else { + println("Copying file: ${item.name}") + copyFile(item, targetItem) + } + } + } + + println("Sprite files copied successfully to internal storage: ${internalDir.absolutePath}") + + } catch (e: Exception) { + println("Error copying sprite files to internal storage: ${e.message}") + e.printStackTrace() + } + } + fun copySpriteFilesToExternalStorage() { try { println("Starting sprite file copy process to external storage...") @@ -55,7 +106,7 @@ class SpriteFileManager(private val context: Context) { } // Create the base directory for battle_sprites in external storage - val battleSpritesDir = getSpriteBaseDir() + val battleSpritesDir = getExternalSpriteBaseDir() if (!battleSpritesDir.exists()) { battleSpritesDir.mkdirs() println("Created battle_sprites directory in external storage: ${battleSpritesDir.absolutePath}") @@ -171,6 +222,39 @@ class SpriteFileManager(private val context: Context) { } } + private fun copyDirectory(sourceDir: File, targetDir: File) { + if (!targetDir.exists()) { + targetDir.mkdirs() + } + + val files = sourceDir.listFiles() + if (files != null) { + files.forEach { file -> + val targetFile = File(targetDir, file.name) + if (file.isDirectory) { + copyDirectory(file, targetFile) + } else { + copyFile(file, targetFile) + } + } + } + } + + private fun copyFile(sourceFile: File, targetFile: File) { + try { + val inputStream = FileInputStream(sourceFile) + val outputStream = FileOutputStream(targetFile) + + inputStream.copyTo(outputStream) + inputStream.close() + outputStream.close() + + println("Copied: ${sourceFile.name} -> ${targetFile.absolutePath}") + } catch (e: IOException) { + println("Error copying file ${sourceFile.name}: ${e.message}") + } + } + private fun copyAssetFile(assetPath: String, targetFile: File) { try { val inputStream = context.assets.open(assetPath) @@ -187,7 +271,7 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val battleSpritesDir = getSpriteBaseDir() + val battleSpritesDir = getInternalSpriteBaseDir() val extractedAssetsDir = File(battleSpritesDir, "extracted_assets") val extractedStatsDir = File(battleSpritesDir, "extracted_digimon_stats") val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") @@ -199,7 +283,7 @@ class SpriteFileManager(private val context: Context) { val atkspritesExist = atkspritesDir.exists() && atkspritesDir.listFiles()?.isNotEmpty() == true val battlebgsExist = battlebgsDir.exists() && battlebgsDir.listFiles()?.isNotEmpty() == true - println("Checking sprite files exist:") + println("Checking sprite files exist in internal storage:") println(" battle_sprites exists: $battleSpritesExist") println(" extracted_assets exists: $assetsExist") println(" extracted_digimon_stats exists: $statsExist") @@ -211,11 +295,11 @@ class SpriteFileManager(private val context: Context) { fun clearSpriteFiles() { try { - val battleSpritesDir = getSpriteBaseDir() + val battleSpritesDir = getInternalSpriteBaseDir() if (battleSpritesDir.exists()) { deleteDirectory(battleSpritesDir) - println("Cleared battle_sprites directory") + println("Cleared battle_sprites directory from internal storage") } } catch (e: Exception) { 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 11c39a6..8f50f31 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 @@ -1602,10 +1602,10 @@ fun BattlesScreen() { println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") val spriteFileManager = SpriteFileManager(context) if (!spriteFileManager.checkSpriteFilesExist()) { - println("BATTLESCREEN: Copying sprite files to external storage...") - spriteFileManager.copySpriteFilesToExternalStorage() + println("BATTLESCREEN: Copying sprite files from external storage to internal storage...") + spriteFileManager.copySpriteFilesToInternalStorage() } else { - println("BATTLESCREEN: Sprite files already exist in external storage") + println("BATTLESCREEN: Sprite files already exist in internal storage") } } @@ -2265,11 +2265,10 @@ fun AnimatedBattleBackground( println("DEBUG: Screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load background image from external storage + // Load background image from internal storage LaunchedEffect(Unit) { try { - val externalDir = Environment.getExternalStorageDirectory() - val backgroundFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + val backgroundFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") if (backgroundFile.exists()) { backgroundBitmap = BitmapFactory.decodeFile(backgroundFile.absolutePath) println("Successfully loaded battle background: ${backgroundFile.absolutePath}") @@ -2372,14 +2371,13 @@ fun MultiLayerAnimatedBattleBackground( println("DEBUG: Multi-layer screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load all three background layers from external storage + // Load all three background layers from internal storage LaunchedEffect(backgroundSetIndex) { try { - val externalDir = Environment.getExternalStorageDirectory() val selectedSet = backgroundSets[backgroundSetIndex] // Back layer - val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") + val backLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") if (backLayerFile.exists()) { backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) println("Successfully loaded back layer background (Set ${backgroundSetIndex + 1}): ${backLayerFile.absolutePath}") @@ -2388,7 +2386,7 @@ fun MultiLayerAnimatedBattleBackground( } // Middle layer - val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") + val middleLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") if (middleLayerFile.exists()) { middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) println("Successfully loaded middle layer background (Set ${backgroundSetIndex + 1}): ${middleLayerFile.absolutePath}") @@ -2397,7 +2395,7 @@ fun MultiLayerAnimatedBattleBackground( } // Front layer - val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") + val frontLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") if (frontLayerFile.exists()) { frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) println("Successfully loaded front layer background (Set ${backgroundSetIndex + 1}): ${frontLayerFile.absolutePath}") From 952fd5a8714c0313c8abf35b2da003552c003414 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 12:02:14 -0400 Subject: [PATCH 68/89] Added scrollable button list for opponents of all stages. --- .../vbhelper/screens/BattlesScreen.kt | 224 +++++++++++------- 1 file changed, 136 insertions(+), 88 deletions(-) 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 8f50f31..1231ebd 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 @@ -1973,160 +1973,208 @@ fun BattlesScreen() { "rookie" -> { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() ) { - Text("Rookie Battle View") + Text("Rookie Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) // Add character selection dropdown characterDropdown("rookie") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> - currentView = "battle-main" - } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") - } - } - // Show selected character info activeCharacter?.let { character -> - Text("Active Character: ${character.name}") + Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) Text("HP: ${character.currentHp}/${character.baseHp}") Text("BP: ${character.baseBp}") Text("AP: ${character.baseAp}") } + Spacer(modifier = Modifier.height(16.dp)) + + // Scrollable list of opponents + Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(opponentsList) { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> + currentView = "battle-main" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Battle ${opponent.name}") + } + } + } + backButton() } } "champion" -> { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() ) { - Text("Champion Battle View") + Text("Champion Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) // Add character selection dropdown characterDropdown("champion") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> - currentView = "battle-main" - } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") - } - } - // Show selected character info activeCharacter?.let { character -> - Text("Active Character: ${character.name}") + Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) Text("HP: ${character.currentHp}/${character.baseHp}") Text("BP: ${character.baseBp}") Text("AP: ${character.baseAp}") } + Spacer(modifier = Modifier.height(16.dp)) + + // Scrollable list of opponents + Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(opponentsList) { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> + currentView = "battle-main" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Battle ${opponent.name}") + } + } + } + backButton() } } "ultimate" -> { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() ) { - Text("Ultimate Battle View") + Text("Ultimate Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) // Add character selection dropdown characterDropdown("ultimate") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> - currentView = "battle-main" - } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") - } - } - // Show selected character info activeCharacter?.let { character -> - Text("Active Character: ${character.name}") + Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) Text("HP: ${character.currentHp}/${character.baseHp}") Text("BP: ${character.baseBp}") Text("AP: ${character.baseAp}") } + Spacer(modifier = Modifier.height(16.dp)) + + // Scrollable list of opponents + Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(opponentsList) { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> + currentView = "battle-main" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Battle ${opponent.name}") + } + } + } + backButton() } } "mega" -> { Column( - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() ) { - Text("Mega Battle View") + Text("Mega Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) // Add character selection dropdown characterDropdown("mega") - // Display buttons for each opponent - opponentsList.forEach { opponent -> - Button( - onClick = { - activeCharacter?.let { - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> - currentView = "battle-main" - } - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text("Battle ${opponent.name}") - } - } - // Show selected character info activeCharacter?.let { character -> - Text("Active Character: ${character.name}") + Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) Text("HP: ${character.currentHp}/${character.baseHp}") Text("BP: ${character.baseBp}") Text("AP: ${character.baseAp}") } + Spacer(modifier = Modifier.height(16.dp)) + + // Scrollable list of opponents + Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(opponentsList) { opponent -> + Button( + onClick = { + activeCharacter?.let { + selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) + RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> + currentView = "battle-main" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Battle ${opponent.name}") + } + } + } + backButton() } } From 14e941031ca0c36e451251d7fad9e48a831eddb6 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 12:14:32 -0400 Subject: [PATCH 69/89] Updated API to send charaID instead of character name for match setup. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 1231ebd..3b500b6 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 @@ -2007,7 +2007,7 @@ fun BattlesScreen() { selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 0, 0, opponent.name, 0) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 0, 0, opponent.charaId, 0) { apiResult -> currentView = "battle-main" } } @@ -2059,7 +2059,7 @@ fun BattlesScreen() { selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 1, 0, opponent.name, 1) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 1, 0, opponent.charaId, 1) { apiResult -> currentView = "battle-main" } } @@ -2111,7 +2111,7 @@ fun BattlesScreen() { selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 2, 0, opponent.name, 2) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 2, 0, opponent.charaId, 2) { apiResult -> currentView = "battle-main" } } @@ -2163,7 +2163,7 @@ fun BattlesScreen() { selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.name, 3, 0, opponent.name, 3) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 3, 0, opponent.charaId, 3) { apiResult -> currentView = "battle-main" } } From 61daad459b327751f7768d46acd9f2f848ad8c32 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 12:55:57 -0400 Subject: [PATCH 70/89] Setup player API call to pull from active Digi. --- .../vbhelper/screens/BattlesScreen.kt | 170 +++++++++++++----- 1 file changed, 126 insertions(+), 44 deletions(-) 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 3b500b6..34bc814 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 @@ -71,6 +71,10 @@ import java.io.File import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.foundation.layout.width +import com.github.nacabaro.vbhelper.di.VBHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun isLandscapeMode(): Boolean { @@ -1545,6 +1549,8 @@ fun BattlesScreen() { var activeCharacter by remember { mutableStateOf(null) } var selectedOpponent by remember { mutableStateOf(null) } + var activeUserCharacter by remember { mutableStateOf(null) } + var activeCardId by remember { mutableStateOf(null) } var expanded by remember { mutableStateOf(false) } var selectedStage by remember { mutableStateOf("") } @@ -1608,6 +1614,66 @@ fun BattlesScreen() { println("BATTLESCREEN: Sprite files already exist in internal storage") } } + + // Load active character from database + LaunchedEffect(Unit) { + try { + val application = context.applicationContext as VBHelper + val database = application.container.db + + // Move database operations to background thread + kotlinx.coroutines.withContext(Dispatchers.IO) { + // First, let's check all characters to see what's in the database + val allCharacters = database.userCharacterDao().getAllCharacters() + println("BATTLESCREEN: Found ${allCharacters.size} total characters in database") + allCharacters.forEach { char -> + println(" - Character ID: ${char.id}, CharId: ${char.charId}") + } + + val activeChar = database.userCharacterDao().getActiveCharacter() + println("BATTLESCREEN: getActiveCharacter() returned: $activeChar") + + if (activeChar != null) { + // Get the character data using the charId from activeChar + val characterData = database.characterDao().getCharacterInfo(activeChar.charId) + println("BATTLESCREEN: CharacterData from getCharacterInfo:") + println(" - cardId: ${characterData.cardId}") + println(" - charId: ${characterData.charId}") + println(" - stage: ${characterData.stage}") + println(" - attribute: ${characterData.attribute}") + + // The cardId from getCharacterInfo is already the correct card ID we need! + val cardId = characterData.cardId + val charaIndex = characterData.charId // This is the charaIndex from the query + + // Format as "dim" + cardId + "_mon" + (charaIndex + 1) + val formattedCardId = String.format("dim%03d_mon%02d", cardId, charaIndex + 1) + + // Update UI state on main thread + withContext(Dispatchers.Main) { + activeUserCharacter = activeChar + activeCardId = formattedCardId + } + + println("BATTLESCREEN: Loaded active character from database:") + println(" - UserCharacter ID: ${activeChar.id}") + println(" - CharId: ${activeChar.charId}") + println(" - CharacterData cardId: ${characterData.cardId}") + println(" - CharacterData charaIndex: $charaIndex") + println(" - Final cardId: $cardId") + println(" - Formatted as: $activeCardId") + } else { + println("BATTLESCREEN: No active character found in database") + withContext(Dispatchers.Main) { + activeCardId = null + } + } + } + } catch (e: Exception) { + println("BATTLESCREEN: Error loading active character: ${e.message}") + e.printStackTrace() + } + } val rookieButton = @Composable { Button( @@ -1978,15 +2044,17 @@ fun BattlesScreen() { ) { Text("Rookie Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - // Add character selection dropdown - characterDropdown("rookie") - - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") + // Show active character info from database + activeUserCharacter?.let { character -> + Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Char ID: ${character.charId}") + Text("Stage: ${character.stage}") + Text("Age: ${character.ageInDays} days") + activeCardId?.let { cardId -> + Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) + } + } ?: run { + Text("No active character found in database", fontSize = 16.sp, color = Color.Red) } Spacer(modifier = Modifier.height(16.dp)) @@ -2003,13 +2071,15 @@ fun BattlesScreen() { items(opponentsList) { opponent -> Button( onClick = { - activeCharacter?.let { + activeCardId?.let { cardId -> selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 0, 0, opponent.charaId, 0) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 0, 0, opponent.charaId, 0) { apiResult -> currentView = "battle-main" } + } ?: run { + println("BATTLESCREEN: No active card ID found in database") } }, modifier = Modifier.fillMaxWidth() @@ -2030,15 +2100,17 @@ fun BattlesScreen() { ) { Text("Champion Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - // Add character selection dropdown - characterDropdown("champion") - - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") + // Show active character info from database + activeUserCharacter?.let { character -> + Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Char ID: ${character.charId}") + Text("Stage: ${character.stage}") + Text("Age: ${character.ageInDays} days") + activeCardId?.let { cardId -> + Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) + } + } ?: run { + Text("No active character found in database", fontSize = 16.sp, color = Color.Red) } Spacer(modifier = Modifier.height(16.dp)) @@ -2055,13 +2127,15 @@ fun BattlesScreen() { items(opponentsList) { opponent -> Button( onClick = { - activeCharacter?.let { + activeCardId?.let { cardId -> selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 1, 0, opponent.charaId, 1) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 1, 0, opponent.charaId, 1) { apiResult -> currentView = "battle-main" } + } ?: run { + println("BATTLESCREEN: No active card ID found in database") } }, modifier = Modifier.fillMaxWidth() @@ -2082,15 +2156,17 @@ fun BattlesScreen() { ) { Text("Ultimate Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - // Add character selection dropdown - characterDropdown("ultimate") - - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") + // Show active character info from database + activeUserCharacter?.let { character -> + Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Char ID: ${character.charId}") + Text("Stage: ${character.stage}") + Text("Age: ${character.ageInDays} days") + activeCardId?.let { cardId -> + Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) + } + } ?: run { + Text("No active character found in database", fontSize = 16.sp, color = Color.Red) } Spacer(modifier = Modifier.height(16.dp)) @@ -2107,13 +2183,15 @@ fun BattlesScreen() { items(opponentsList) { opponent -> Button( onClick = { - activeCharacter?.let { + activeCardId?.let { cardId -> selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 2, 0, opponent.charaId, 2) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 2, 0, opponent.charaId, 2) { apiResult -> currentView = "battle-main" } + } ?: run { + println("BATTLESCREEN: No active card ID found in database") } }, modifier = Modifier.fillMaxWidth() @@ -2134,15 +2212,17 @@ fun BattlesScreen() { ) { Text("Mega Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - // Add character selection dropdown - characterDropdown("mega") - - // Show selected character info - activeCharacter?.let { character -> - Text("Active Character: ${character.name}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("HP: ${character.currentHp}/${character.baseHp}") - Text("BP: ${character.baseBp}") - Text("AP: ${character.baseAp}") + // Show active character info from database + activeUserCharacter?.let { character -> + Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Char ID: ${character.charId}") + Text("Stage: ${character.stage}") + Text("Age: ${character.ageInDays} days") + activeCardId?.let { cardId -> + Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) + } + } ?: run { + Text("No active character found in database", fontSize = 16.sp, color = Color.Red) } Spacer(modifier = Modifier.height(16.dp)) @@ -2159,13 +2239,15 @@ fun BattlesScreen() { items(opponentsList) { opponent -> Button( onClick = { - activeCharacter?.let { + activeCardId?.let { cardId -> selectedOpponent = opponent // Randomly select background set (0, 1, or 2) selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, it.charaId, 3, 0, opponent.charaId, 3) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 3, 0, opponent.charaId, 3) { apiResult -> currentView = "battle-main" } + } ?: run { + println("BATTLESCREEN: No active card ID found in database") } }, modifier = Modifier.fillMaxWidth() From 0875b114d55df69c1385490a19a5ae9a322fb794 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 13:09:14 -0400 Subject: [PATCH 71/89] Fixed HP desync with battle. --- .../vbhelper/screens/BattlesScreen.kt | 268 ++++++++++-------- 1 file changed, 151 insertions(+), 117 deletions(-) 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 34bc814..822ed0a 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 @@ -835,9 +835,9 @@ fun MiddleBattleView( verticalArrangement = Arrangement.SpaceEvenly ) { // Enemy Digimon (top half) - Box( - modifier = Modifier - .fillMaxWidth() + Box( + modifier = Modifier + .fillMaxWidth() .weight(1f), contentAlignment = Alignment.CenterEnd ) { @@ -846,8 +846,8 @@ fun MiddleBattleView( battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1 battleSystem.isOpponentDodging -> DigimonAnimationType.WALK battleSystem.isOpponentHitDelayed -> DigimonAnimationType.SLEEP - else -> DigimonAnimationType.IDLE - } + else -> DigimonAnimationType.IDLE + } // Calculate vertical offset for enemy dodge animation val enemyVerticalOffset = if (battleSystem.isOpponentDodging) { @@ -873,17 +873,17 @@ fun MiddleBattleView( } else { 0.dp } - - AnimatedSpriteImage( + + AnimatedSpriteImage( characterId = opponentCharacter?.charaId ?: "dim011_mon01", animationType = enemyAnimationType, - modifier = Modifier - .size(80.dp) + modifier = Modifier + .size(80.dp) .offset( x = enemyHitOffset, y = enemyVerticalOffset + 40.dp ), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit, reloadMappings = false, animationOffset = 375L // Offset enemy animation by half the idle duration ) @@ -1101,22 +1101,22 @@ fun MiddleBattleView( // Match is still ongoing - update HP and continue println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Set pending damage based on API result + // Set pending damage based on API result if (apiResult.playerAttackDamage > 0) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) // Also check if enemy counter-attacks and hits if (apiResult.opponentAttackDamage > 0) { println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage } - } else { + } else { // Player attack missed - enemy counter-attacks println("Player attack missed! Enemy counter-attacks") - battleSystem.setAttackHitState(false) + battleSystem.setAttackHitState(false) // Set up counter-attack - determine if it hits based on API result val counterAttackHits = apiResult.opponentAttackDamage > 0 println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") @@ -1137,7 +1137,7 @@ fun MiddleBattleView( battleSystem.setupCounterAttack(finalCounterAttackHits) // Set the opponent attack hit state for Phase 3 battleSystem.handleOpponentAttackResult(finalCounterAttackHits) - } + } } 2 -> { // Match is over - transition to results screen @@ -1199,7 +1199,7 @@ fun PlayerBattleView( MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize(), backgroundSetIndex = selectedBackgroundSet) // Top section: HP bar and HP numbers - Column( + Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) @@ -1241,19 +1241,19 @@ fun PlayerBattleView( // Middle section: Player Digimon only Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp), contentAlignment = Alignment.Center - ) { + ) { // Player Digimon (left side) - Box( - modifier = Modifier - .fillMaxWidth() - .size(80.dp), + Box( + modifier = Modifier + .fillMaxWidth() + .size(80.dp), contentAlignment = Alignment.CenterStart - ) { - // Determine animation type based on battle state + ) { + // Determine animation type based on battle state val animationType = when { battleSystem.isPlayerDodging -> DigimonAnimationType.WALK // Use walk animation for dodge battleSystem.isPlayerHitDelayed -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) @@ -1261,8 +1261,8 @@ fun PlayerBattleView( battleSystem.attackPhase == 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen battleSystem.attackPhase == 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen battleSystem.attackPhase == 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen - else -> DigimonAnimationType.IDLE - } + else -> DigimonAnimationType.IDLE + } // Calculate vertical offset for dodge animation val verticalOffset = if (battleSystem.isPlayerDodging) { @@ -1291,10 +1291,10 @@ fun PlayerBattleView( } else { 0.dp } - - AnimatedSpriteImage( - characterId = activeCharacter?.charaId ?: "dim011_mon01", - animationType = animationType, + + AnimatedSpriteImage( + characterId = activeCharacter?.charaId ?: "dim011_mon01", + animationType = animationType, modifier = Modifier .size(80.dp) .scale(-1f, 1f) // Flip player Digimon horizontally @@ -1302,59 +1302,59 @@ fun PlayerBattleView( x = hitOffset, y = verticalOffset ), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Fit, reloadMappings = false, animationOffset = 0L // Player animation starts immediately - ) - - // Attack sprite visibility and positioning based on attack phase - val shouldShowAttack = when (battleSystem.attackPhase) { + ) + + // Attack sprite visibility and positioning based on attack phase + val shouldShowAttack = when (battleSystem.attackPhase) { 1 -> false // Both attacks from middle screen 2 -> false // Player attack on enemy screen 3 -> true // Enemy attack on player screen - else -> false + else -> false + } + + if (shouldShowAttack) { + val xOffset = when (battleSystem.attackPhase) { + 3 -> (-attackAnimationProgress * 400 + 350).dp // Enemy attack on player screen - start more to the right + else -> 0.dp } - if (shouldShowAttack) { - val xOffset = when (battleSystem.attackPhase) { - 3 -> (-attackAnimationProgress * 400 + 350).dp // Enemy attack on player screen - start more to the right - else -> 0.dp - } - // Use opponent character ID for Phase 3 (enemy attack) - val characterId = when (battleSystem.attackPhase) { + val characterId = when (battleSystem.attackPhase) { 3 -> opponent?.charaId ?: "dim011_mon01" // Use opponent's character ID else -> activeCharacter?.charaId ?: "dim011_mon01" // Use player's character ID + } + + // Handle sprite transition + LaunchedEffect(characterId, battleSystem.attackPhase) { + if ((previousCharacterId != null && previousCharacterId != characterId) || + (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { + // Character ID or attack phase changed, start transition + isTransitioning = true + delay(100) // Brief invisibility period + isTransitioning = false } - - // Handle sprite transition - LaunchedEffect(characterId, battleSystem.attackPhase) { - if ((previousCharacterId != null && previousCharacterId != characterId) || - (previousAttackPhase != null && previousAttackPhase != battleSystem.attackPhase)) { - // Character ID or attack phase changed, start transition - isTransitioning = true - delay(100) // Brief invisibility period - isTransitioning = false - } - previousCharacterId = characterId - previousAttackPhase = battleSystem.attackPhase - } - + previousCharacterId = characterId + previousAttackPhase = battleSystem.attackPhase + } + println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - + if (!isTransitioning && !hidePlayerAttackSprite) { - AttackSpriteImage( - characterId = characterId, - isLarge = true, - modifier = Modifier - .size(60.dp) - .offset( - x = xOffset, - y = 0.dp - ) + AttackSpriteImage( + characterId = characterId, + isLarge = true, + modifier = Modifier + .size(60.dp) + .offset( + x = xOffset, + y = 0.dp + ) .scale(1f, 1f), // Don't flip enemy attacks on player screen - contentScale = ContentScale.Fit - ) + contentScale = ContentScale.Fit + ) } } } @@ -1400,19 +1400,19 @@ fun EnemyBattleView( Column( horizontalAlignment = getLandscapeHorizontalAlignment() ) { - // Enemy HP bar - LinearProgressIndicator( - progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + // Enemy HP bar + LinearProgressIndicator( + progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), modifier = getLandscapeModifier(), - color = Color.Red, - trackColor = Color.Gray - ) + color = Color.Red, + trackColor = Color.Gray + ) Spacer(modifier = Modifier.height(4.dp)) - // Enemy HP display numbers - Text( - text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", + // Enemy HP display numbers + Text( + text = "Enemy HP: ${battleSystem.opponentHP.toInt()}/${activeCharacter?.baseHp ?: 100}", fontSize = getLandscapeFontSize(), color = Color.White, style = TextStyle( @@ -1649,10 +1649,24 @@ fun BattlesScreen() { // Format as "dim" + cardId + "_mon" + (charaIndex + 1) val formattedCardId = String.format("dim%03d_mon%02d", cardId, charaIndex + 1) + // Create APIBattleCharacter from database character + val playerCharacter = APIBattleCharacter( + name = "Player Digimon", // We could get this from the database if needed + namekey = "player_digimon", // Name key for the character + charaId = formattedCardId, // Use the formatted card ID for sprite loading + stage = characterData.stage, + attribute = characterData.attribute.ordinal, // Convert enum to int + baseHp = 1000, // Default values - API will provide correct values + currentHp = 1000, + baseBp = 1000.0f, + baseAp = 1000.0f + ) + // Update UI state on main thread withContext(Dispatchers.Main) { activeUserCharacter = activeChar activeCardId = formattedCardId + activeCharacter = playerCharacter // Set the active character for battle } println("BATTLESCREEN: Loaded active character from database:") @@ -2069,25 +2083,30 @@ 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) RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 0, 0, opponent.charaId, 0) { apiResult -> - currentView = "battle-main" - } + // Update player character HP from API response + 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}") } } + } backButton() } @@ -2125,25 +2144,30 @@ 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) RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 1, 0, opponent.charaId, 1) { apiResult -> + // Update player character HP from API response + 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}") } } + } backButton() } @@ -2181,25 +2205,30 @@ 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) RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 2, 0, opponent.charaId, 2) { apiResult -> + // Update player character HP from API response + 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}") } } + } backButton() } @@ -2237,25 +2266,30 @@ 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) RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 3, 0, opponent.charaId, 3) { apiResult -> + // Update player character HP from API response + 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}") } } + } backButton() } From 5ddb8f5da94f8f13b99a27fe5a61b1869225f123 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 13:32:41 -0400 Subject: [PATCH 72/89] Battle screen now filters opponents automatically based on active Digimon's stage. --- .../vbhelper/screens/BattlesScreen.kt | 474 +++++------------- 1 file changed, 128 insertions(+), 346 deletions(-) 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 822ed0a..1da9a96 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 @@ -1603,6 +1603,53 @@ fun BattlesScreen() { val context = LocalContext.current + // Determine if player can battle based on stage (derived from activeUserCharacter) + val canBattle = activeUserCharacter?.stage?.let { it >= 2 } ?: false + + // Get the appropriate battle type based on player's stage (derived from activeUserCharacter) + val playerBattleType = activeUserCharacter?.stage?.let { stage -> + when (stage) { + 2 -> "rookie" // Player stage 2 → Rookie opponents (API stage 0) + 3 -> "champion" // Player stage 3 → Champion opponents (API stage 1) + 4 -> "ultimate" // Player stage 4 → Ultimate opponents (API stage 2) + 5 -> "mega" // Player stage 5 → Mega opponents (API stage 3) + else -> null + } + } + + // Load opponents automatically based on player's stage + LaunchedEffect(activeUserCharacter) { + val currentCharacter = activeUserCharacter + if (currentCharacter != null && canBattle && playerBattleType != null) { + println("BATTLESCREEN: Loading opponents for stage ${currentCharacter.stage}, battle type: $playerBattleType") + try { + RetrofitHelper().getOpponents(context, playerBattleType!!) { opponents -> + try { + // Create a new list to trigger UI recomposition + opponentsList = ArrayList(opponents.opponentsList) + println("BATTLESCREEN: Loaded ${opponents.opponentsList.size} opponents from API") + println("BATTLESCREEN: Total opponents in list: ${opponentsList.size}") + } 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() + } + } else { + println("BATTLESCREEN: Cannot load opponents - activeUserCharacter: $currentCharacter") + println("BATTLESCREEN: canBattle: $canBattle") + println("BATTLESCREEN: playerBattleType: $playerBattleType") + println("BATTLESCREEN: currentCharacter != null: ${currentCharacter != null}") + if (currentCharacter != null) { + println("BATTLESCREEN: currentCharacter.stage: ${currentCharacter.stage}") + println("BATTLESCREEN: currentCharacter.stage >= 2: ${currentCharacter.stage >= 2}") + } + } + } + // Initialize sprite files on first load LaunchedEffect(Unit) { println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") @@ -1672,10 +1719,13 @@ fun BattlesScreen() { println("BATTLESCREEN: Loaded active character from database:") println(" - UserCharacter ID: ${activeChar.id}") println(" - CharId: ${activeChar.charId}") + println(" - Stage: ${activeChar.stage}") println(" - CharacterData cardId: ${characterData.cardId}") println(" - CharacterData charaIndex: $charaIndex") println(" - Final cardId: $cardId") println(" - Formatted as: $activeCardId") + println(" - Can battle: ${activeChar.stage >= 2}") + println(" - Battle type: ${when (activeChar.stage) { 2 -> "rookie"; 3 -> "champion"; 4 -> "ultimate"; 5 -> "mega"; else -> "none" }}") } else { println("BATTLESCREEN: No active character found in database") withContext(Dispatchers.Main) { @@ -1689,105 +1739,6 @@ fun BattlesScreen() { } } - val rookieButton = @Composable { - Button( - onClick = { - try { - RetrofitHelper().getOpponents(context, "rookie") { opponents -> - try { - opponentsList.clear() - opponentsList.addAll(opponents.opponentsList) - currentView = "rookie" - currentStage = "rookie" - } 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() - } - } - ) { - Text("Rookie Battles") - } - } - - val championButton = @Composable { - Button( - onClick = { - try { - RetrofitHelper().getOpponents(context, "champion") { opponents -> - try { - opponentsList.clear() - opponentsList.addAll(opponents.opponentsList) - currentView = "champion" - currentStage = "champion" - } 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() - } - } - ) { - Text("Champion Battles") - } - } - - val ultimateButton = @Composable { - Button( - onClick = { - try { - RetrofitHelper().getOpponents(context, "ultimate") { opponents -> - try { - opponentsList.clear() - opponentsList.addAll(opponents.opponentsList) - currentView = "ultimate" - currentStage = "ultimate" - } 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() - } - } - ) { - Text("Ultimate Battles") - } - } - - val megaButton = @Composable { - Button( - onClick = { - try { - RetrofitHelper().getOpponents(context, "mega") { opponents -> - try { - opponentsList.clear() - opponentsList.addAll(opponents.opponentsList) - currentView = "mega" - currentStage = "mega" - } 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() - } - } - ) { - Text("Mega Battles") - } - } val backButton = @Composable { Button( @@ -2020,10 +1971,84 @@ fun BattlesScreen() { Column( horizontalAlignment = Alignment.CenterHorizontally ) { - rookieButton() - championButton() - ultimateButton() - megaButton() + // Show active character info + activeUserCharacter?.let { character -> + Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Stage: ${character.stage}") + Text("Age: ${character.ageInDays} days") + activeCardId?.let { cardId -> + Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (canBattle) { + Text("Available Opponents:", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text("Debug: opponentsList.size = ${opponentsList.size}", fontSize = 12.sp, color = Color.Gray) + Spacer(modifier = Modifier.height(8.dp)) + + if (opponentsList.isNotEmpty()) { + println("BATTLESCREEN: UI - Showing ${opponentsList.size} opponents") + // Show scrollable list of opponents + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(opponentsList) { opponent -> + Button( + onClick = { + activeCardId?.let { cardId -> + selectedOpponent = opponent + // Randomly select background set (0, 1, or 2) + selectedBackgroundSet = kotlin.random.Random.nextInt(3) + + // Determine the correct stage parameter for API call + val apiStage = when (playerBattleType) { + "rookie" -> 0 + "champion" -> 1 + "ultimate" -> 2 + "mega" -> 3 + else -> 0 + } + + RetrofitHelper().getPVPWinner(context, 0, 2, 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" + } + } ?: run { + println("BATTLESCREEN: No active card ID found in database") + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Battle ${opponent.name}") + } + } + } + } else { + println("BATTLESCREEN: UI - No opponents in list, showing message") + Text("No opponents available for your stage", + fontSize = 16.sp, + color = Color(0xFFFFA500), // Orange color + textAlign = TextAlign.Center) + } + } else { + Text("Your Digimon must be at least Stage 2 to battle", + fontSize = 16.sp, + color = Color.Red, + textAlign = TextAlign.Center) + } + } ?: run { + Text("No active character found in database", fontSize = 16.sp, color = Color.Red) + } + /* Button( onClick = { @@ -2051,249 +2076,6 @@ fun BattlesScreen() { } } - "rookie" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text("Rookie Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - - // Show active character info from database - activeUserCharacter?.let { character -> - Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("Char ID: ${character.charId}") - Text("Stage: ${character.stage}") - Text("Age: ${character.ageInDays} days") - activeCardId?.let { cardId -> - Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) - } - } ?: run { - Text("No active character found in database", fontSize = 16.sp, color = Color.Red) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Scrollable list of opponents - Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(opponentsList) { opponent -> - Button( - onClick = { - activeCardId?.let { cardId -> - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 0, 0, opponent.charaId, 0) { apiResult -> - // Update player character HP from API response - 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}") - } - } - } - - backButton() - } - } - - "champion" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text("Champion Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - - // Show active character info from database - activeUserCharacter?.let { character -> - Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("Char ID: ${character.charId}") - Text("Stage: ${character.stage}") - Text("Age: ${character.ageInDays} days") - activeCardId?.let { cardId -> - Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) - } - } ?: run { - Text("No active character found in database", fontSize = 16.sp, color = Color.Red) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Scrollable list of opponents - Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(opponentsList) { opponent -> - Button( - onClick = { - activeCardId?.let { cardId -> - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 1, 0, opponent.charaId, 1) { apiResult -> - // Update player character HP from API response - 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}") - } - } - } - - backButton() - } - } - - "ultimate" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text("Ultimate Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - - // Show active character info from database - activeUserCharacter?.let { character -> - Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("Char ID: ${character.charId}") - Text("Stage: ${character.stage}") - Text("Age: ${character.ageInDays} days") - activeCardId?.let { cardId -> - Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) - } - } ?: run { - Text("No active character found in database", fontSize = 16.sp, color = Color.Red) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Scrollable list of opponents - Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(opponentsList) { opponent -> - Button( - onClick = { - activeCardId?.let { cardId -> - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 2, 0, opponent.charaId, 2) { apiResult -> - // Update player character HP from API response - 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}") - } - } - } - - backButton() - } - } - - "mega" -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text("Mega Battle View", fontSize = 20.sp, fontWeight = FontWeight.Bold) - - // Show active character info from database - activeUserCharacter?.let { character -> - Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Text("Char ID: ${character.charId}") - Text("Stage: ${character.stage}") - Text("Age: ${character.ageInDays} days") - activeCardId?.let { cardId -> - Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) - } - } ?: run { - Text("No active character found in database", fontSize = 16.sp, color = Color.Red) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Scrollable list of opponents - Text("Select Opponent:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(opponentsList) { opponent -> - Button( - onClick = { - activeCardId?.let { cardId -> - selectedOpponent = opponent - // Randomly select background set (0, 1, or 2) - selectedBackgroundSet = kotlin.random.Random.nextInt(3) - RetrofitHelper().getPVPWinner(context, 0, 2, cardId, 3, 0, opponent.charaId, 3) { apiResult -> - // Update player character HP from API response - 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}") - } - } - } - - backButton() - } - } "battle-main" -> { BattleScreen( From 0f1feb88b8e654d0ddddb89b485e8b6fd11f5435 Mon Sep 17 00:00:00 2001 From: lightheel Date: Sun, 19 Oct 2025 17:36:20 -0400 Subject: [PATCH 73/89] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 78e9b77..028aaa6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,6 @@ app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex app/src/main/java/com/github/nacabaro/vbhelper/battle/Battle_Sprites_Reference/ app/src/main/assets/battle_sprites +app/src/main/assets/extracted_audio API-ACR122USAM-2.01.pdf \ No newline at end of file From b4d509aad9be25e7b8795e0de11522de7890a1d4 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 11:11:58 -0500 Subject: [PATCH 74/89] Swapped from using internal app storage to android device storage. --- app/build.gradle.kts | 7 + app/src/main/AndroidManifest.xml | 2 + .../vbhelper/battle/AttackSpriteManager.kt | 17 +-- .../vbhelper/battle/BattleSpriteManager.kt | 5 +- .../vbhelper/battle/HitEffectSpriteManager.kt | 5 +- .../battle/IndividualSpriteManager.kt | 5 +- .../vbhelper/battle/SpriteFileManager.kt | 6 +- .../vbhelper/screens/BattlesScreen.kt | 134 +++++++++++++++--- 8 files changed, 148 insertions(+), 33 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7e4a241..02b7baf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -92,4 +92,11 @@ dependencies { implementation(libs.protobuf.javalite) implementation("androidx.compose.material:material") implementation("androidx.datastore:datastore-preferences:1.1.7") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") + + // HTTP request logging + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8da12c..8c72cce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + () - // Get the internal storage directory for attack sprites - private fun getAttackTexturesPath(): String { - return "battle_sprites/extracted_atksprites" + // Get the external storage directory for attack sprites + private fun getAttackTexturesBaseDir(): File { + val externalDir = android.os.Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_atksprites") } fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? { @@ -58,9 +59,8 @@ class AttackSpriteManager(private val context: Context) { return null } - // Load the attack sprite from internal storage - val attackFilePath = "${getAttackTexturesPath()}/$attackFileName.png" - val attackFile = File(context.filesDir, attackFilePath) + // Load the attack sprite from external storage + val attackFile = File(getAttackTexturesBaseDir(), "$attackFileName.png") println("AttackSpriteManager: Attack file path = ${attackFile.absolutePath}") println("AttackSpriteManager: Attack file exists = ${attackFile.exists()}") @@ -88,8 +88,9 @@ class AttackSpriteManager(private val context: Context) { } try { - // Load character data from JSON file in internal storage - val characterDataFile = File(context.filesDir, "battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") + // Load character data from JSON file in external storage + val externalDir = android.os.Environment.getExternalStorageDirectory() + val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt index 7f1708b..1f1224f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt @@ -38,9 +38,10 @@ class BattleSpriteManager(private val context: Context) { private val gson = Gson() private val spriteCache = mutableMapOf() - // Get the internal storage directory for sprite files + // Get the external storage directory for sprite files private fun getSpriteBaseDir(): File { - return File(context.filesDir, "battle_sprites/extracted_assets") + val externalDir = android.os.Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_assets") } fun loadSprite(spriteName: String, atlasName: String): Bitmap? { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt index b7fabfa..c33205c 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -10,9 +10,10 @@ import java.io.File class HitEffectSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Get the internal storage directory for hit effect sprites + // Get the external storage directory for hit effect sprites private fun getHitSpritesDir(): File { - return File(context.filesDir, "battle_sprites/extracted_hit_sprites") + val externalDir = android.os.Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_hit_sprites") } /** diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt index 09a87e7..56b664f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt @@ -9,9 +9,10 @@ import java.io.File class IndividualSpriteManager(private val context: Context) { private val spriteCache = mutableMapOf() - // Get the internal storage directory for sprite files + // Get the external storage directory for sprite files private fun getSpriteBaseDir(): File { - return File(context.filesDir, "battle_sprites/extracted_assets/sprites") + val externalDir = android.os.Environment.getExternalStorageDirectory() + return File(externalDir, "VBHelper/battle_sprites/extracted_assets/sprites") } /** diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 1baa2cd..40a4db0 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -9,7 +9,7 @@ import java.io.FileInputStream class SpriteFileManager(private val context: Context) { // Get the external storage directory where files are already located - private fun getExternalSpriteBaseDir(): File { + fun getExternalSpriteBaseDir(): File { val externalDir = android.os.Environment.getExternalStorageDirectory() return File(externalDir, "VBHelper/battle_sprites") } @@ -271,7 +271,7 @@ class SpriteFileManager(private val context: Context) { } fun checkSpriteFilesExist(): Boolean { - val battleSpritesDir = getInternalSpriteBaseDir() + val battleSpritesDir = getExternalSpriteBaseDir() val extractedAssetsDir = File(battleSpritesDir, "extracted_assets") val extractedStatsDir = File(battleSpritesDir, "extracted_digimon_stats") val atkspritesDir = File(battleSpritesDir, "extracted_atksprites") @@ -283,7 +283,7 @@ class SpriteFileManager(private val context: Context) { val atkspritesExist = atkspritesDir.exists() && atkspritesDir.listFiles()?.isNotEmpty() == true val battlebgsExist = battlebgsDir.exists() && battlebgsDir.listFiles()?.isNotEmpty() == true - println("Checking sprite files exist in internal storage:") + println("Checking sprite files exist in external storage:") println(" battle_sprites exists: $battleSpritesExist") println(" extracted_assets exists: $assetsExist") println(" extracted_digimon_stats exists: $statsExist") 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 1da9a96..91271e8 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 @@ -34,6 +34,16 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.Box import androidx.compose.ui.platform.LocalContext import androidx.compose.runtime.LaunchedEffect +import android.Manifest +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.activity.ComponentActivity +import android.os.Build +import android.provider.Settings +import android.content.Intent +import android.net.Uri import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import kotlinx.coroutines.delay @@ -1542,6 +1552,92 @@ fun EnemyBattleView( @Composable fun BattlesScreen() { val TAG = "BattleScreen" + val context = LocalContext.current + val activity = context as? ComponentActivity + + // Permission state + var hasStoragePermission by remember { mutableStateOf(false) } + + // Check if permission is already granted + // For Android 11+ (API 30+), check MANAGE_EXTERNAL_STORAGE + // For Android 10 and below, check READ_EXTERNAL_STORAGE + val permissionCheck = remember { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11+ - check for MANAGE_EXTERNAL_STORAGE + android.os.Environment.isExternalStorageManager() + } else { + // Android 10 and below - check for READ_EXTERNAL_STORAGE + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } + } + + // Permission launcher for READ_EXTERNAL_STORAGE (Android 10 and below) + val readStoragePermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + hasStoragePermission = isGranted + if (isGranted) { + println("BATTLESCREEN: READ_EXTERNAL_STORAGE permission granted") + } else { + println("BATTLESCREEN: READ_EXTERNAL_STORAGE permission denied") + } + } + + // Launcher for opening settings to grant MANAGE_EXTERNAL_STORAGE (Android 11+) + val manageStorageSettingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + // Re-check permission after returning from settings + val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + android.os.Environment.isExternalStorageManager() + } else { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } + hasStoragePermission = hasPermission + if (hasPermission) { + println("BATTLESCREEN: MANAGE_EXTERNAL_STORAGE permission granted") + } else { + println("BATTLESCREEN: MANAGE_EXTERNAL_STORAGE permission not granted") + } + } + + // Initialize permission state + LaunchedEffect(Unit) { + hasStoragePermission = permissionCheck + if (!permissionCheck && activity != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11+ - need to request MANAGE_EXTERNAL_STORAGE + // This requires user to go to settings + println("BATTLESCREEN: Android 11+ detected - opening settings for MANAGE_EXTERNAL_STORAGE") + try { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.parse("package:${context.packageName}") + manageStorageSettingsLauncher.launch(intent) + } catch (e: Exception) { + println("BATTLESCREEN: Error opening settings: ${e.message}") + // Fallback: try the general manage external storage settings + try { + val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + manageStorageSettingsLauncher.launch(intent) + } catch (e2: Exception) { + println("BATTLESCREEN: Error opening fallback settings: ${e2.message}") + } + } + } else { + // Android 10 and below - request READ_EXTERNAL_STORAGE + println("BATTLESCREEN: Requesting READ_EXTERNAL_STORAGE permission...") + readStoragePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } else if (permissionCheck) { + println("BATTLESCREEN: Storage permission already granted") + } + } var currentView by remember { mutableStateOf("main") } @@ -1601,8 +1697,6 @@ fun BattlesScreen() { else -> rookieCharacters } - val context = LocalContext.current - // Determine if player can battle based on stage (derived from activeUserCharacter) val canBattle = activeUserCharacter?.stage?.let { it >= 2 } ?: false @@ -1650,15 +1744,19 @@ fun BattlesScreen() { } } - // Initialize sprite files on first load - LaunchedEffect(Unit) { - println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") - val spriteFileManager = SpriteFileManager(context) - if (!spriteFileManager.checkSpriteFilesExist()) { - println("BATTLESCREEN: Copying sprite files from external storage to internal storage...") - spriteFileManager.copySpriteFilesToInternalStorage() + // Initialize sprite files on first load - check that they exist in external storage + // Only check if permission is granted + LaunchedEffect(hasStoragePermission) { + if (hasStoragePermission) { + println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") + val spriteFileManager = SpriteFileManager(context) + if (spriteFileManager.checkSpriteFilesExist()) { + println("BATTLESCREEN: Sprite files exist in external storage") + } else { + println("BATTLESCREEN: Sprite files not found in external storage") + } } else { - println("BATTLESCREEN: Sprite files already exist in internal storage") + println("BATTLESCREEN: Cannot check sprite files - storage permission not granted") } } @@ -1672,10 +1770,12 @@ fun BattlesScreen() { kotlinx.coroutines.withContext(Dispatchers.IO) { // First, let's check all characters to see what's in the database val allCharacters = database.userCharacterDao().getAllCharacters() + /* println("BATTLESCREEN: Found ${allCharacters.size} total characters in database") allCharacters.forEach { char -> println(" - Character ID: ${char.id}, CharId: ${char.charId}") } + */ val activeChar = database.userCharacterDao().getActiveCharacter() println("BATTLESCREEN: getActiveCharacter() returned: $activeChar") @@ -2211,10 +2311,11 @@ fun AnimatedBattleBackground( println("DEBUG: Screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load background image from internal storage + // Load background image from external storage LaunchedEffect(Unit) { try { - val backgroundFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") + val externalDir = android.os.Environment.getExternalStorageDirectory() + val backgroundFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") if (backgroundFile.exists()) { backgroundBitmap = BitmapFactory.decodeFile(backgroundFile.absolutePath) println("Successfully loaded battle background: ${backgroundFile.absolutePath}") @@ -2317,13 +2418,14 @@ fun MultiLayerAnimatedBattleBackground( println("DEBUG: Multi-layer screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } - // Load all three background layers from internal storage + // Load all three background layers from external storage LaunchedEffect(backgroundSetIndex) { try { + val externalDir = android.os.Environment.getExternalStorageDirectory() val selectedSet = backgroundSets[backgroundSetIndex] // Back layer - val backLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") + val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") if (backLayerFile.exists()) { backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) println("Successfully loaded back layer background (Set ${backgroundSetIndex + 1}): ${backLayerFile.absolutePath}") @@ -2332,7 +2434,7 @@ fun MultiLayerAnimatedBattleBackground( } // Middle layer - val middleLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") + val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") if (middleLayerFile.exists()) { middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) println("Successfully loaded middle layer background (Set ${backgroundSetIndex + 1}): ${middleLayerFile.absolutePath}") @@ -2341,7 +2443,7 @@ fun MultiLayerAnimatedBattleBackground( } // Front layer - val frontLayerFile = File(context.filesDir, "battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") + val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") if (frontLayerFile.exists()) { frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) println("Successfully loaded front layer background (Set ${backgroundSetIndex + 1}): ${frontLayerFile.absolutePath}") From 29ff2805c33cae5700b2e4165c05e3ea25095a60 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 14:31:18 -0500 Subject: [PATCH 75/89] Added NacaAuth for logging into Battles. --- app/src/main/AndroidManifest.xml | 13 + .../nacabaro/vbhelper/battle/AuthService.kt | 11 + .../vbhelper/battle/AuthenticateRequest.kt | 6 + .../vbhelper/battle/AuthenticateResponse.kt | 7 + .../vbhelper/battle/BattleAuthContainer.kt | 16 ++ .../vbhelper/battle/RetrofitHelper.kt | 67 +++++ .../vbhelper/screens/BattlesScreen.kt | 272 +++++++++++++++++- .../vbhelper/source/AuthRepository.kt | 45 +++ 8 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateRequest.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleAuthContainer.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c72cce..8e59f52 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt new file mode 100644 index 0000000..11f7b74 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt @@ -0,0 +1,11 @@ +package com.github.nacabaro.vbhelper.battle + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface AuthService { + @POST("api/auth/validate") + fun validate(@Body request: AuthenticateRequest): Call +} + diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateRequest.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateRequest.kt new file mode 100644 index 0000000..d1391c3 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateRequest.kt @@ -0,0 +1,6 @@ +package com.github.nacabaro.vbhelper.battle + +data class AuthenticateRequest( + val userToken: String +) + diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt new file mode 100644 index 0000000..3f8e13f --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt @@ -0,0 +1,7 @@ +package com.github.nacabaro.vbhelper.battle + +data class AuthenticateResponse( + val success: Boolean, + val message: String? = null +) + diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleAuthContainer.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleAuthContainer.kt new file mode 100644 index 0000000..6024048 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleAuthContainer.kt @@ -0,0 +1,16 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.github.nacabaro.vbhelper.source.AuthRepository + +private const val BATTLE_AUTH_PREFERENCES_NAME = "battle_auth_preferences" +val Context.battleAuthStore: androidx.datastore.core.DataStore by preferencesDataStore( + name = BATTLE_AUTH_PREFERENCES_NAME +) + +class BattleAuthContainer(private val context: Context) { + val authRepository: AuthRepository = AuthRepository(context.battleAuthStore) +} + 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 1cfca0e..1dca1ae 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 @@ -5,6 +5,8 @@ import retrofit2.Retrofit import android.widget.Toast import retrofit2.* import retrofit2.converter.gson.GsonConverterFactory +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor class RetrofitHelper { @@ -176,4 +178,69 @@ class RetrofitHelper { } }) } + + fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) { + println("RetrofitHelper: Starting validate API call with token: $token") + + if (token.isEmpty()) { + println("RetrofitHelper: ERROR - Token is empty!") + Toast.makeText(context, "Authentication failed: Token is empty", Toast.LENGTH_SHORT).show() + return + } + + try { + // Add logging interceptor to see the actual HTTP request + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + val retrofit: Retrofit = Retrofit.Builder() + .baseUrl("http://192.168.0.230:8080/") + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val service: AuthService = retrofit.create(AuthService::class.java) + val request = AuthenticateRequest(userToken = token) + println("RetrofitHelper: Sending request to api/auth/validate with userToken: $token") + println("RetrofitHelper: Request object: $request") + println("RetrofitHelper: Request JSON will be: {\"userToken\": \"$token\"}") + val call: Call = service.validate(request) + + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + println("RetrofitHelper: Validate API call failed: ${t.message}") + t.printStackTrace() + Toast.makeText(context, "Authentication failed: ${t.message}", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + println("RetrofitHelper: Validate API response received - Code: ${response.code()}") + println("RetrofitHelper: Response body: ${response.body()}") + + if (response.isSuccessful) { + val authResponse: AuthenticateResponse? = response.body() + if (authResponse != null) { + println("RetrofitHelper: Validation successful: ${authResponse.success}, message: ${authResponse.message}") + callback(authResponse) + } else { + println("RetrofitHelper: Validation failed: Invalid response body") + Toast.makeText(context, "Authentication failed: Invalid response", Toast.LENGTH_SHORT).show() + } + } else { + val errorBody = response.errorBody()?.string() + println("RetrofitHelper: Validate response not successful - Code: ${response.code()}, Error: $errorBody") + Toast.makeText(context, "Authentication failed: ${response.code()}", Toast.LENGTH_SHORT).show() + } + } + }) + } catch (e: Exception) { + println("RetrofitHelper: Exception in validate: ${e.message}") + e.printStackTrace() + Toast.makeText(context, "Authentication failed: ${e.message}", Toast.LENGTH_SHORT).show() + } + } } \ No newline at end of file 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 91271e8..b5177ce 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 @@ -34,6 +34,10 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.Box import androidx.compose.ui.platform.LocalContext import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import android.Manifest import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult @@ -63,6 +67,8 @@ import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage import com.github.nacabaro.vbhelper.battle.HitEffectOverlay +import com.github.nacabaro.vbhelper.battle.BattleAuthContainer +import kotlinx.coroutines.flow.first import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import kotlin.math.sin @@ -1640,6 +1646,15 @@ fun BattlesScreen() { } var currentView by remember { mutableStateOf("main") } + + // Create BattleAuthContainer + val battleAuthContainer = remember { BattleAuthContainer(context) } + + // Auth state + var isAuthenticated by remember { mutableStateOf(false) } + var isCheckingAuth by remember { mutableStateOf(true) } + // Track processed tokens to prevent duplicate API calls + var processedTokens by remember { mutableStateOf>(emptySet()) } var opponentsList by remember { mutableStateOf(ArrayList()) } @@ -1712,7 +1727,14 @@ fun BattlesScreen() { } // Load opponents automatically based on player's stage - LaunchedEffect(activeUserCharacter) { + // Only load if authenticated and character is ready + LaunchedEffect(activeUserCharacter, isAuthenticated) { + // Wait for authentication to complete before loading opponents + if (!isAuthenticated) { + println("BATTLESCREEN: Skipping opponent load - not authenticated yet") + return@LaunchedEffect + } + val currentCharacter = activeUserCharacter if (currentCharacter != null && canBattle && playerBattleType != null) { println("BATTLESCREEN: Loading opponents for stage ${currentCharacter.stage}, battle type: $playerBattleType") @@ -1744,6 +1766,221 @@ fun BattlesScreen() { } } + // Helper lambda to extract token from URI and authenticate + // The token can be in 'c' parameter (from localhost:8080/authenticate?c=...) or 'token' parameter + val handleTokenFromUri: (Uri) -> Unit = { uri -> + // Try 'c' parameter first (from localhost:8080/authenticate?c=...) + var token = uri.getQueryParameter("c") + // Fall back to 'token' parameter if 'c' is not found + if (token == null || token.isEmpty()) { + token = uri.getQueryParameter("token") + } + + if (token != null && token.isNotEmpty()) { + // Check if we've already processed this token + if (!processedTokens.contains(token)) { + // Mark token as being processed + processedTokens = processedTokens + token + println("BATTLESCREEN: Received token from URI: $token (URI: $uri)") + + // Exchange token with battle server + RetrofitHelper().authenticate(context, token) { response -> + if (response.success) { + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + battleAuthContainer.authRepository.setAuthenticated(true, token) + } + // Update UI state on main thread + kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { + isAuthenticated = true + isCheckingAuth = false + println("BATTLESCREEN: Authentication successful") + android.widget.Toast.makeText(context, "Authentication successful!", android.widget.Toast.LENGTH_SHORT).show() + } + } else { + println("BATTLESCREEN: Authentication failed: ${response.message}") + // Show toast on main thread + kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { + android.widget.Toast.makeText(context, "Authentication failed: ${response.message}", android.widget.Toast.LENGTH_SHORT).show() + } + // Remove token from processed set on failure so it can be retried if needed + processedTokens = processedTokens - token + } + } + } else { + println("BATTLESCREEN: Token already processed, skipping: $token") + } + } else { + println("BATTLESCREEN: No token found in URI: $uri (checked 'c' and 'token' parameters)") + } + } + + // Check authentication status on load + LaunchedEffect(Unit) { + try { + val authRepository = battleAuthContainer.authRepository + val localAuthState = authRepository.isAuthenticated.first() + val storedToken = authRepository.authToken.first() + println("BATTLESCREEN: Local authentication status - isAuthenticated: $localAuthState, hasToken: ${storedToken != null}") + + // Only check for token in intent if it's a fresh deep link (ACTION_VIEW intent) + // This prevents processing stale tokens from previous sessions + val activity = context as? ComponentActivity + val intent = activity?.intent + if (intent?.action == Intent.ACTION_VIEW) { + intent.data?.let { uri -> + println("BATTLESCREEN: Found ACTION_VIEW intent with URI: $uri") + if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { + println("BATTLESCREEN: Token found in fresh deep link, processing...") + handleTokenFromUri(uri) + return@LaunchedEffect // Don't open auth URL if we're processing a token + } + } + } + + // If we have a stored token, validate it with the server + if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { + println("BATTLESCREEN: Validating stored token with server...") + RetrofitHelper().authenticate(context, storedToken) { response -> + // Update UI on main thread + kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { + if (response.success) { + println("BATTLESCREEN: Token validation successful") + isAuthenticated = true + isCheckingAuth = false + } else { + println("BATTLESCREEN: Token validation failed: ${response.message}") + // Clear authentication state + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + authRepository.logout() + } + isAuthenticated = false + isCheckingAuth = false + // Open auth URL + val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" + val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) + context.startActivity(authIntent) + println("BATTLESCREEN: Opened auth URL after validation failure: $authUrl") + } + } + } + } else { + // No stored token or not authenticated locally + isAuthenticated = false + isCheckingAuth = false + // If not authenticated and no fresh token in intent, open auth URL + val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" + val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) + context.startActivity(authIntent) + println("BATTLESCREEN: Opened auth URL: $authUrl") + } + } catch (e: Exception) { + println("BATTLESCREEN: Error checking authentication status: ${e.message}") + isAuthenticated = false + isCheckingAuth = false + } + } + + // Handle deep link callback to get token + // Check intent data on initial load - handle both vbhelper:// and http://localhost:8080/authenticate?c= + // Only process if it's a fresh ACTION_VIEW intent (deep link) + LaunchedEffect(Unit) { + // Small delay to ensure activity is fully initialized + kotlinx.coroutines.delay(100) + + val activity = context as? ComponentActivity + val intent = activity?.intent + + // Only process if this is a fresh deep link (ACTION_VIEW) + if (intent?.action == Intent.ACTION_VIEW) { + val uri = intent.data + if (uri != null) { + println("BATTLESCREEN: Checking ACTION_VIEW intent data - URI: $uri, scheme: ${uri.scheme}, host: ${uri.host}, path: ${uri.path}") + println("BATTLESCREEN: All query parameters: ${uri.queryParameterNames}") + + // Handle vbhelper://auth?token= or vbhelper://auth?c= deep link + if (uri.scheme == "vbhelper" && uri.host == "auth") { + println("BATTLESCREEN: Detected vbhelper://auth deep link") + handleTokenFromUri(uri) + } + // Handle http://localhost:8080/authenticate?c= redirect + else if ((uri.scheme == "http" || uri.scheme == "https") && + (uri.host == "localhost" || uri.host == "127.0.0.1" || uri.host?.contains("8080") == true)) { + println("BATTLESCREEN: Detected localhost redirect, checking for token") + handleTokenFromUri(uri) + } + // Also check if there's a 'c' or 'token' parameter in any URL + else if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { + println("BATTLESCREEN: Found token parameter (c or token) in URI, attempting to authenticate") + handleTokenFromUri(uri) + } else { + println("BATTLESCREEN: URI found but no token parameter detected") + } + } else { + println("BATTLESCREEN: ACTION_VIEW intent but no URI found") + } + } else { + println("BATTLESCREEN: Not an ACTION_VIEW intent, skipping deep link processing") + } + } + + // Check intent when screen becomes visible or when authentication state changes + // This handles cases where the app is already running and receives a deep link + DisposableEffect(Unit) { + val activity = context as? ComponentActivity + val lifecycleOwner = activity as? LifecycleOwner + + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME && !isAuthenticated) { + // Check intent data when activity resumes - only if it's a fresh ACTION_VIEW intent + val intent = activity?.intent + if (intent?.action == Intent.ACTION_VIEW) { + intent.data?.let { uri -> + println("BATTLESCREEN: Activity resumed with ACTION_VIEW intent, checking for token - URI: $uri") + if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { + println("BATTLESCREEN: Found token in fresh deep link on resume") + handleTokenFromUri(uri) + } + } + } + } + } + + lifecycleOwner?.lifecycle?.addObserver(observer) + + onDispose { + lifecycleOwner?.lifecycle?.removeObserver(observer) + } + } + + // Also check intent when authentication state changes + // Only process if it's a fresh ACTION_VIEW intent (deep link) + LaunchedEffect(isAuthenticated) { + if (!isAuthenticated) { + kotlinx.coroutines.delay(200) // Small delay to ensure intent is available + val activity = context as? ComponentActivity + val intent = activity?.intent + // Only process if this is a fresh deep link (ACTION_VIEW) + if (intent?.action == Intent.ACTION_VIEW) { + intent.data?.let { uri -> + println("BATTLESCREEN: Re-checking ACTION_VIEW intent data - URI: $uri, scheme: ${uri.scheme}, host: ${uri.host}") + // Handle vbhelper://auth?token= or vbhelper://auth?c= deep link + if (uri.scheme == "vbhelper" && uri.host == "auth") { + handleTokenFromUri(uri) + } + // Handle http://localhost:8080/authenticate?c= redirect + else if ((uri.scheme == "http" || uri.scheme == "https") && + (uri.host == "localhost" || uri.host == "127.0.0.1" || uri.host?.contains("8080") == true)) { + handleTokenFromUri(uri) + } + // Also check if there's a 'c' or 'token' parameter in any URL + else if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { + handleTokenFromUri(uri) + } + } + } + } + } + // Initialize sprite files on first load - check that they exist in external storage // Only check if permission is granted LaunchedEffect(hasStoragePermission) { @@ -2061,7 +2298,38 @@ fun BattlesScreen() { ) { when (currentView) { "main" -> { - if (showSpriteTester) { + // Show loading/authentication message if not authenticated + if (isCheckingAuth || !isAuthenticated) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(32.dp) + ) { + if (isCheckingAuth) { + Text( + text = "Checking authentication...", + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } else { + Text( + text = "Please complete authentication in your browser", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 16.dp) + ) + Text( + text = "You will be redirected back to the app after logging in", + fontSize = 14.sp, + color = Color.Gray, + textAlign = TextAlign.Center + ) + } + } + } else if (showSpriteTester) { when (spriteTesterView) { "entry" -> spriteTesterEntry() "testing" -> spriteTesterTesting() diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt new file mode 100644 index 0000000..a9327eb --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt @@ -0,0 +1,45 @@ +package com.github.nacabaro.vbhelper.source + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class AuthRepository( + private val dataStore: DataStore +) { + private companion object { + val IS_AUTHENTICATED = booleanPreferencesKey("is_authenticated") + val AUTH_TOKEN = stringPreferencesKey("auth_token") + } + + val isAuthenticated: Flow = dataStore.data + .map { preferences -> + preferences[IS_AUTHENTICATED] ?: false + } + + val authToken: Flow = dataStore.data + .map { preferences -> + preferences[AUTH_TOKEN] + } + + suspend fun setAuthenticated(isAuthenticated: Boolean, token: String? = null) { + dataStore.edit { preferences -> + preferences[IS_AUTHENTICATED] = isAuthenticated + if (token != null) { + preferences[AUTH_TOKEN] = token + } + } + } + + suspend fun logout() { + dataStore.edit { preferences -> + preferences[IS_AUTHENTICATED] = false + preferences.remove(AUTH_TOKEN) + } + } +} + From b0c5d6375d9f7a73bafdd63bb9381bf15fc867a9 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 15:06:42 -0500 Subject: [PATCH 76/89] Setup API calls to use User ID. --- .../vbhelper/battle/AuthenticateResponse.kt | 15 ++++- .../vbhelper/screens/BattlesScreen.kt | 56 ++++++++++++++----- .../vbhelper/source/AuthRepository.kt | 13 ++++- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt index 3f8e13f..942791d 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt @@ -1,7 +1,20 @@ package com.github.nacabaro.vbhelper.battle +data class AdditionalInfo( + val avatar: String? = null, + val id: Long? = null, + val name: String? = null, + val status: String? = null +) + +data class UserInfo( + val userId: String? = null, + val additionalInfo: AdditionalInfo? = null +) + data class AuthenticateResponse( val success: Boolean, - val message: String? = null + val message: String? = null, + val userInfo: UserInfo? = null ) 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 b5177ce..c5bea91 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 @@ -218,6 +218,7 @@ fun AnimatedDamageNumber( @Composable fun BattleScreen( + userId: Long? = null, stage: String, playerName: String, opponentName: String, @@ -227,6 +228,9 @@ fun BattleScreen( context: android.content.Context? = null, selectedBackgroundSet: Int = 0 ) { + // Capture userId parameter for use in lambdas - use remember to ensure it's accessible in all scopes + val currentUserId = remember { userId } + val battleSystem = remember { ArenaBattleSystem() } val coroutineScope = rememberCoroutineScope() @@ -621,6 +625,7 @@ fun BattleScreen( 0 -> { // Middle screen - both Digimon MiddleBattleView( + userId = currentUserId, battleSystem = battleSystem, stage = stage, playerName = playerName, @@ -757,6 +762,7 @@ fun BattleScreen( @Composable fun MiddleBattleView( + userId: Long? = null, battleSystem: ArenaBattleSystem, stage: String, playerName: String, @@ -1072,6 +1078,9 @@ fun MiddleBattleView( onClick = { println("Attack button clicked!") + // Capture userId for use in this lambda + val playerUserId = userId + // Get crit bar progress as float (0.0f to 100.0f) val critBarProgressFloat = battleSystem.critBarProgress.toFloat() @@ -1100,7 +1109,7 @@ fun MiddleBattleView( RetrofitHelper().getPVPWinner( ctx, 1, - 2, + playerUserId?.toInt() ?: 2, activeCharacter?.name ?: "Player", playerStage, opponentStage, @@ -1653,6 +1662,7 @@ fun BattlesScreen() { // Auth state var isAuthenticated by remember { mutableStateOf(false) } var isCheckingAuth by remember { mutableStateOf(true) } + var userId by remember { mutableStateOf(null) } // Track processed tokens to prevent duplicate API calls var processedTokens by remember { mutableStateOf>(emptySet()) } @@ -1777,37 +1787,40 @@ fun BattlesScreen() { } if (token != null && token.isNotEmpty()) { - // Check if we've already processed this token + // Check if we've already successfully processed this token if (!processedTokens.contains(token)) { - // Mark token as being processed - processedTokens = processedTokens + token println("BATTLESCREEN: Received token from URI: $token (URI: $uri)") // Exchange token with battle server RetrofitHelper().authenticate(context, token) { response -> if (response.success) { + // Only mark as processed after successful authentication + processedTokens = processedTokens + token + + // Extract userId from response + val extractedUserId = response.userInfo?.userId?.toLongOrNull() kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { - battleAuthContainer.authRepository.setAuthenticated(true, token) + battleAuthContainer.authRepository.setAuthenticated(true, token, extractedUserId) } // Update UI state on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { isAuthenticated = true isCheckingAuth = false - println("BATTLESCREEN: Authentication successful") + userId = extractedUserId + println("BATTLESCREEN: Authentication successful, userId: $extractedUserId") android.widget.Toast.makeText(context, "Authentication successful!", android.widget.Toast.LENGTH_SHORT).show() } } else { println("BATTLESCREEN: Authentication failed: ${response.message}") + // Don't mark as processed on failure - allow retry with a new token // Show toast on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { android.widget.Toast.makeText(context, "Authentication failed: ${response.message}", android.widget.Toast.LENGTH_SHORT).show() } - // Remove token from processed set on failure so it can be retried if needed - processedTokens = processedTokens - token } } } else { - println("BATTLESCREEN: Token already processed, skipping: $token") + println("BATTLESCREEN: Token already successfully processed, skipping: $token") } } else { println("BATTLESCREEN: No token found in URI: $uri (checked 'c' and 'token' parameters)") @@ -1820,7 +1833,13 @@ fun BattlesScreen() { val authRepository = battleAuthContainer.authRepository val localAuthState = authRepository.isAuthenticated.first() val storedToken = authRepository.authToken.first() - println("BATTLESCREEN: Local authentication status - isAuthenticated: $localAuthState, hasToken: ${storedToken != null}") + val storedUserId = authRepository.userId.first() + println("BATTLESCREEN: Local authentication status - isAuthenticated: $localAuthState, hasToken: ${storedToken != null}, userId: $storedUserId") + + // Load stored userId if available + if (storedUserId != null) { + userId = storedUserId + } // Only check for token in intent if it's a fresh deep link (ACTION_VIEW intent) // This prevents processing stale tokens from previous sessions @@ -1844,9 +1863,17 @@ fun BattlesScreen() { // Update UI on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { if (response.success) { - println("BATTLESCREEN: Token validation successful") + val extractedUserId = response.userInfo?.userId?.toLongOrNull() ?: storedUserId + // Update stored userId if we got a new one + if (extractedUserId != null && extractedUserId != storedUserId) { + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + authRepository.setAuthenticated(true, storedToken, extractedUserId) + } + } + println("BATTLESCREEN: Token validation successful, userId: $extractedUserId") isAuthenticated = true isCheckingAuth = false + userId = extractedUserId } else { println("BATTLESCREEN: Token validation failed: ${response.message}") // Clear authentication state @@ -2382,7 +2409,7 @@ fun BattlesScreen() { else -> 0 } - RetrofitHelper().getPVPWinner(context, 0, 2, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult -> + RetrofitHelper().getPVPWinner(context, 0, userId?.toInt() ?: 2, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult -> // Update player character HP from API response activeCharacter = activeCharacter?.copy( baseHp = apiResult.playerHP, @@ -2447,6 +2474,7 @@ fun BattlesScreen() { "battle-main" -> { BattleScreen( + userId = userId, stage = currentStage, playerName = activeCharacter?.name ?: "Player", opponentName = selectedOpponent?.name ?: "Opponent", @@ -2487,7 +2515,7 @@ fun BattlesScreen() { RetrofitHelper().getPVPWinner( context, 1, - 2, + userId?.toInt() ?: 2, activeCharacter?.name ?: "Player", playerStage, opponentStage, @@ -2501,7 +2529,7 @@ fun BattlesScreen() { RetrofitHelper().getPVPWinner( context, 2, - 2, + userId?.toInt() ?: 2, activeCharacter?.name ?: "Player", playerStage, opponentStage, diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt index a9327eb..c630ca9 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -14,6 +15,7 @@ class AuthRepository( private companion object { val IS_AUTHENTICATED = booleanPreferencesKey("is_authenticated") val AUTH_TOKEN = stringPreferencesKey("auth_token") + val USER_ID = longPreferencesKey("user_id") } val isAuthenticated: Flow = dataStore.data @@ -26,12 +28,20 @@ class AuthRepository( preferences[AUTH_TOKEN] } - suspend fun setAuthenticated(isAuthenticated: Boolean, token: String? = null) { + val userId: Flow = dataStore.data + .map { preferences -> + preferences[USER_ID] + } + + suspend fun setAuthenticated(isAuthenticated: Boolean, token: String? = null, userId: Long? = null) { dataStore.edit { preferences -> preferences[IS_AUTHENTICATED] = isAuthenticated if (token != null) { preferences[AUTH_TOKEN] = token } + if (userId != null) { + preferences[USER_ID] = userId + } } } @@ -39,6 +49,7 @@ class AuthRepository( dataStore.edit { preferences -> preferences[IS_AUTHENTICATED] = false preferences.remove(AUTH_TOKEN) + preferences.remove(USER_ID) } } } From 67b56b39900579170efb8c8e530117a1baccfa77 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 16:37:59 -0500 Subject: [PATCH 77/89] Reduced logging. --- .../vbhelper/battle/RetrofitHelper.kt | 16 +- .../vbhelper/battle/SpriteFileManager.kt | 4 +- .../vbhelper/screens/BattlesScreen.kt | 420 +++--------------- 3 files changed, 82 insertions(+), 358 deletions(-) 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 1dca1ae..90836a3 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 @@ -11,23 +11,23 @@ import okhttp3.logging.HttpLoggingInterceptor class RetrofitHelper { fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) { - println("RetrofitHelper: Starting API call for stage: $stage") + //println("RetrofitHelper: Starting API call for stage: $stage") try { // Create a Retrofit instance with the base URL and // a GsonConverterFactory for parsing the response. val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( GsonConverterFactory.create()).build() - println("RetrofitHelper: Retrofit instance created") + //println("RetrofitHelper: Retrofit instance created") // Create an ApiService instance from the Retrofit instance. val service: OpponentService = retrofit.create(OpponentService::class.java) - println("RetrofitHelper: Service created") + //println("RetrofitHelper: Service created") // Call the getopponents() method of the ApiService // to make an API request. val call: Call = service.getopponents(stage) - println("RetrofitHelper: API call created, enqueueing...") + //println("RetrofitHelper: API call created, enqueueing...") // Use the enqueue() method of the Call object to // make an asynchronous API request. @@ -43,7 +43,7 @@ class RetrofitHelper { println("RetrofitHelper: Response body: ${response.body()}") if(response.isSuccessful){ - println("RetrofitHelper: Response successful, calling callback") + //println("RetrofitHelper: Response successful, calling callback") val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel callback(opponentsList) } else { @@ -180,7 +180,7 @@ class RetrofitHelper { } fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) { - println("RetrofitHelper: Starting validate API call with token: $token") + //println("RetrofitHelper: Starting validate API call with token: $token") if (token.isEmpty()) { println("RetrofitHelper: ERROR - Token is empty!") @@ -206,8 +206,8 @@ class RetrofitHelper { val service: AuthService = retrofit.create(AuthService::class.java) val request = AuthenticateRequest(userToken = token) println("RetrofitHelper: Sending request to api/auth/validate with userToken: $token") - println("RetrofitHelper: Request object: $request") - println("RetrofitHelper: Request JSON will be: {\"userToken\": \"$token\"}") + //println("RetrofitHelper: Request object: $request") + //println("RetrofitHelper: Request JSON will be: {\"userToken\": \"$token\"}") val call: Call = service.validate(request) call.enqueue(object : Callback { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt index 40a4db0..65238e4 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt @@ -282,13 +282,15 @@ class SpriteFileManager(private val context: Context) { val statsExist = extractedStatsDir.exists() && extractedStatsDir.listFiles()?.isNotEmpty() == true val atkspritesExist = atkspritesDir.exists() && atkspritesDir.listFiles()?.isNotEmpty() == true val battlebgsExist = battlebgsDir.exists() && battlebgsDir.listFiles()?.isNotEmpty() == true - + + /* println("Checking sprite files exist in external storage:") println(" battle_sprites exists: $battleSpritesExist") println(" extracted_assets exists: $assetsExist") println(" extracted_digimon_stats exists: $statsExist") println(" extracted_atksprites exists: $atkspritesExist") println(" extracted_battlebgs exists: $battlebgsExist") + */ return battleSpritesExist && assetsExist && statsExist && atkspritesExist && battlebgsExist } 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 c5bea91..824343e 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 @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.Row +//import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size @@ -27,10 +27,10 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.dp import androidx.compose.ui.text.font.FontWeight import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.DropdownMenuItem +//import androidx.compose.material3.ExposedDropdownMenuBox +//import androidx.compose.material3.ExposedDropdownMenuDefaults +//import androidx.compose.material3.OutlinedTextField +//import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.Box import androidx.compose.ui.platform.LocalContext import androidx.compose.runtime.LaunchedEffect @@ -48,8 +48,8 @@ import android.os.Build import android.provider.Settings import android.content.Intent import android.net.Uri -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +//import androidx.compose.animation.core.animateFloatAsState +//import androidx.compose.animation.core.tween import kotlinx.coroutines.delay import kotlinx.coroutines.launch import androidx.compose.runtime.rememberCoroutineScope @@ -71,10 +71,10 @@ import com.github.nacabaro.vbhelper.battle.BattleAuthContainer import kotlinx.coroutines.flow.first import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import kotlin.math.sin -import kotlin.math.PI -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateIntAsState +//import kotlin.math.sin +//import kotlin.math.PI +//import androidx.compose.animation.core.animateDpAsState +//import androidx.compose.animation.core.animateIntAsState import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Shadow @@ -82,14 +82,14 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.foundation.Image import androidx.compose.ui.graphics.asImageBitmap import android.graphics.BitmapFactory -import android.os.Environment +//import android.os.Environment import java.io.File import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.foundation.layout.width import com.github.nacabaro.vbhelper.di.VBHelper import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +//import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable @@ -160,9 +160,7 @@ fun AnimatedDamageNumber( modifier: Modifier = Modifier ) { if (!isVisible) return - - println("DEBUG: AnimatedDamageNumber called with damage=$damage, isVisible=$isVisible") - + var animationProgress by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } var alpha by remember { mutableStateOf(1f) } @@ -170,7 +168,6 @@ fun AnimatedDamageNumber( LaunchedEffect(isVisible) { if (isVisible) { - println("DEBUG: Starting damage number animation for damage=$damage") // Start animation animationProgress = 0f scale = 0.5f @@ -192,7 +189,6 @@ fun AnimatedDamageNumber( yOffset -= 1.dp delay(16) } - println("DEBUG: Damage number animation completed for damage=$damage") } } @@ -267,11 +263,10 @@ fun BattleScreen( showOpponentHitEffect = false hidePlayerAttackSprite = false hideEnemyAttackSprite = false - battleSystem.endPlayerHitDelayed() - battleSystem.endOpponentHitDelayed() - battleSystem.endPlayerShakeDelayed() - battleSystem.endOpponentShakeDelayed() - println("DEBUG: Reset hit effect states, attack sprite visibility, and delayed hit states") + battleSystem.endPlayerHitDelayed() + battleSystem.endOpponentHitDelayed() + battleSystem.endPlayerShakeDelayed() + battleSystem.endOpponentShakeDelayed() } } @@ -290,20 +285,16 @@ fun BattleScreen( when (battleSystem.attackPhase) { 1 -> { // Phase 1: Both attacks from middle screen - println("Starting Phase 1: Both attacks from middle screen") var progress = 0f while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) delay(16) // 60 FPS } - println("Phase 1 completed, advancing to Phase 2") battleSystem.advanceAttackPhase() } 2 -> { // Phase 2: Player attack on enemy screen - println("Starting Phase 2: Player attack on enemy screen") - println("DEBUG: Phase 2 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(2) // Enemy screen var progress = 0f while (progress < 1f) { @@ -314,10 +305,8 @@ fun BattleScreen( if (progress >= 0.50f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { if (battleSystem.attackIsHit) { // Player attack hits enemy - println("Player attack hits enemy at progress $progress") battleSystem.startOpponentHit() // Show hit effect and damage effect - println("DEBUG: Setting showOpponentHitEffect = true (player attack hits enemy)") showOpponentHitEffect = true // Delay hiding the attack sprite to match hit effect timing coroutineScope.launch { @@ -329,31 +318,26 @@ fun BattleScreen( coroutineScope.launch { delay(400) // Match the hit effect delay showOpponentDamageNumber = true - println("DEBUG: Showing opponent damage number after delay") } } // Delay SLEEP animation to match hit effect timing coroutineScope.launch { delay(400) // Match the hit effect delay battleSystem.startOpponentHitDelayed() - println("DEBUG: Starting delayed opponent hit animation") } // Delay shake animation to match hit effect timing coroutineScope.launch { delay(400) // Match the hit effect delay battleSystem.startOpponentShakeDelayed() - println("DEBUG: Starting delayed opponent shake animation") } } else { // Player attack misses, enemy dodges - println("Player attack misses, enemy dodges at progress $progress") battleSystem.startOpponentDodge() } } delay(16) // 60 FPS } - println("Phase 2 completed, applying damage and starting Phase 3") battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) // Hide damage number and reset pending damage after animation @@ -361,24 +345,19 @@ fun BattleScreen( delay(800) // Wait for damage number animation (scale up + hold + fade out) showOpponentDamageNumber = false pendingOpponentDamage = 0f - println("DEBUG: Hiding opponent damage number and resetting pending damage") } delay(100) // Check if there should be a counter-attack if (battleSystem.shouldCounterAttack) { - println("Starting counter-attack from Phase 2") battleSystem.startCounterAttack() } else { - println("No counter-attack, advancing to Phase 3") battleSystem.advanceAttackPhase() } } 3 -> { // Phase 3: Enemy attack on player screen - println("Starting Phase 3: Enemy attack on player screen") - println("DEBUG: Phase 3 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(1) // Player screen var progress = 0f while (progress < 1f) { @@ -387,14 +366,10 @@ fun BattleScreen( // Trigger animation when attack reaches the player (around 50% progress for player dodge) if (progress >= 0.50f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { - println("Phase 3: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}") - println("Phase 3: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") if (battleSystem.opponentAttackIsHit) { // Enemy attack hits player - println("Enemy attack hits player at progress $progress") battleSystem.startPlayerHit() // Show hit effect and damage effect - println("DEBUG: Setting showPlayerHitEffect = true (enemy attack hits player)") showPlayerHitEffect = true // Delay hiding the attack sprite to match hit effect timing coroutineScope.launch { @@ -406,32 +381,26 @@ fun BattleScreen( coroutineScope.launch { delay(400) // Match the hit effect delay showPlayerDamageNumber = true - println("DEBUG: Showing player damage number after delay") } } // Delay SLEEP animation to match hit effect timing coroutineScope.launch { delay(400) // Match the hit effect delay battleSystem.startPlayerHitDelayed() - println("DEBUG: Starting delayed player hit animation") } // Delay shake animation to match hit effect timing coroutineScope.launch { delay(400) // Match the hit effect delay battleSystem.startPlayerShakeDelayed() - println("DEBUG: Starting delayed player shake animation") } } else { // Enemy attack misses, player dodges - println("Enemy attack misses, player dodges at progress $progress") battleSystem.startPlayerDodge() } } delay(16) // 60 FPS } - println("Phase 3 completed, applying damage and resetting") - println("DEBUG: pendingPlayerDamage = $pendingPlayerDamage") battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) // Hide damage number and reset pending damage after animation @@ -439,7 +408,6 @@ fun BattleScreen( delay(800) // Wait for damage number animation (scale up + hold + fade out) showPlayerDamageNumber = false pendingPlayerDamage = 0f - println("DEBUG: Hiding player damage number and resetting pending damage") } battleSystem.resetAttackState() @@ -457,7 +425,6 @@ fun BattleScreen( // Player dodge animation LaunchedEffect(battleSystem.isPlayerDodging) { if (battleSystem.isPlayerDodging) { - println("Starting player dodge animation") var dodgeProgress = 0f var dodgeDirection = 1f // Start moving up @@ -483,14 +450,12 @@ fun BattleScreen( } battleSystem.endPlayerDodge() - println("Player dodge animation completed") } } // Opponent dodge animation LaunchedEffect(battleSystem.isOpponentDodging) { if (battleSystem.isOpponentDodging) { - println("Starting opponent dodge animation") var dodgeProgress = 0f var dodgeDirection = 1f // Start moving up @@ -516,14 +481,12 @@ fun BattleScreen( } battleSystem.endOpponentDodge() - println("Opponent dodge animation completed") } } // Player hit animation LaunchedEffect(battleSystem.isPlayerHit) { if (battleSystem.isPlayerHit) { - println("Starting player hit animation") var hitProgress = 0f // Quick hit effect @@ -536,14 +499,12 @@ fun BattleScreen( delay(100) // Brief pause battleSystem.endPlayerHit() - println("Player hit animation completed") } } // Player delayed shake animation LaunchedEffect(battleSystem.isPlayerShakeDelayed) { if (battleSystem.isPlayerShakeDelayed) { - println("Starting delayed player shake animation") var hitProgress = 0f // Quick hit effect @@ -556,14 +517,12 @@ fun BattleScreen( delay(100) // Brief pause battleSystem.endPlayerShakeDelayed() - println("Delayed player shake animation completed") } } // Opponent hit animation LaunchedEffect(battleSystem.isOpponentHit) { if (battleSystem.isOpponentHit) { - println("Starting opponent hit animation") var hitProgress = 0f // Quick hit effect @@ -576,14 +535,12 @@ fun BattleScreen( delay(100) // Brief pause battleSystem.endOpponentHit() - println("Opponent hit animation completed") } } // Opponent delayed shake animation LaunchedEffect(battleSystem.isOpponentShakeDelayed) { if (battleSystem.isOpponentShakeDelayed) { - println("Starting delayed opponent shake animation") var hitProgress = 0f // Quick hit effect @@ -596,14 +553,12 @@ fun BattleScreen( delay(100) // Brief pause battleSystem.endOpponentShakeDelayed() - println("Delayed opponent shake animation completed") } } // Damage number handling - store pending damage but don't show immediately LaunchedEffect(pendingPlayerDamage) { if (pendingPlayerDamage > 0) { - println("DEBUG: LaunchedEffect triggered for pendingPlayerDamage = $pendingPlayerDamage") playerDamageValue = pendingPlayerDamage.toInt() // Don't show immediately - wait for attack animation to reach the Digimon } @@ -611,7 +566,6 @@ fun BattleScreen( LaunchedEffect(pendingOpponentDamage) { if (pendingOpponentDamage > 0) { - println("DEBUG: LaunchedEffect triggered for pendingOpponentDamage = $pendingOpponentDamage") opponentDamageValue = pendingOpponentDamage.toInt() // Don't show immediately - wait for attack animation to reach the Digimon } @@ -692,7 +646,6 @@ fun BattleScreen( } 1 -> { // Player screen - show player damage (when opponent attacks player) - println("DEBUG: Player screen damage overlay - playerDamageValue=$playerDamageValue, showPlayerDamageNumber=$showPlayerDamageNumber") AnimatedDamageNumber( damage = playerDamageValue, isVisible = showPlayerDamageNumber, @@ -708,31 +661,15 @@ fun BattleScreen( modifier = Modifier.fillMaxSize(), isPlayerScreen = true, onAnimationComplete = { - println("DEBUG: Player hit effect animation completed, setting showPlayerHitEffect = false") showPlayerHitEffect = false hidePlayerAttackSprite = false // Show attack sprite again - println("DEBUG: Player hit effect animation completed") } ) - - // Debug text overlay - /* - Text( - text = "View: ${battleSystem.currentView}, Player Damage: $playerDamageValue, Show: $showPlayerDamageNumber", - color = Color.Red, - fontSize = 12.sp, - modifier = Modifier - .align(Alignment.TopCenter) - .offset(y = 200.dp) - .background(Color.White.copy(alpha = 0.8f)) - ) - */ } 2 -> { // Enemy screen - show opponent damage (when player attacks opponent) - println("DEBUG: Enemy screen damage overlay - opponentDamageValue=$opponentDamageValue, showOpponentDamageNumber=$showOpponentDamageNumber") AnimatedDamageNumber( damage = opponentDamageValue, isVisible = showOpponentDamageNumber, @@ -747,10 +684,8 @@ fun BattleScreen( modifier = Modifier.fillMaxSize(), isPlayerScreen = false, onAnimationComplete = { - println("DEBUG: Enemy hit effect animation completed, setting showOpponentHitEffect = false") showOpponentHitEffect = false hideEnemyAttackSprite = false // Show attack sprite again - println("DEBUG: Enemy hit effect animation completed") } ) @@ -835,14 +770,6 @@ fun MiddleBattleView( Spacer(modifier = Modifier.height(16.dp)) - // Player HP display numbers - /* - Text( - text = "HP: ${battleSystem.playerHP.toInt()}/${activeCharacter?.baseHp ?: 100}", - fontSize = 14.sp, - color = Color.Black - ) - */ } // Middle section: Both Digimon with horizontal line separator @@ -1076,7 +1003,6 @@ fun MiddleBattleView( // Attack button Button( onClick = { - println("Attack button clicked!") // Capture userId for use in this lambda val playerUserId = userId @@ -1365,8 +1291,6 @@ fun PlayerBattleView( previousAttackPhase = battleSystem.attackPhase } - println("PlayerBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - if (!isTransitioning && !hidePlayerAttackSprite) { AttackSpriteImage( characterId = characterId, @@ -1541,8 +1465,6 @@ fun EnemyBattleView( previousAttackPhase = battleSystem.attackPhase } - println("EnemyBattleView - Attack sprite - Phase: ${battleSystem.attackPhase}, Progress: $attackAnimationProgress, X Offset: $xOffset, CurrentView: ${battleSystem.currentView}") - if (!isTransitioning && !hideEnemyAttackSprite) { AttackSpriteImage( characterId = characterId, @@ -1595,7 +1517,6 @@ fun BattlesScreen() { ) { isGranted: Boolean -> hasStoragePermission = isGranted if (isGranted) { - println("BATTLESCREEN: READ_EXTERNAL_STORAGE permission granted") } else { println("BATTLESCREEN: READ_EXTERNAL_STORAGE permission denied") } @@ -1616,7 +1537,6 @@ fun BattlesScreen() { } hasStoragePermission = hasPermission if (hasPermission) { - println("BATTLESCREEN: MANAGE_EXTERNAL_STORAGE permission granted") } else { println("BATTLESCREEN: MANAGE_EXTERNAL_STORAGE permission not granted") } @@ -1650,7 +1570,7 @@ fun BattlesScreen() { readStoragePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } else if (permissionCheck) { - println("BATTLESCREEN: Storage permission already granted") + //println("BATTLESCREEN: Storage permission already granted") } } @@ -1681,13 +1601,16 @@ fun BattlesScreen() { var selectedBackgroundSet by remember { mutableStateOf(0) } // Sprite animation tester state + /* var showSpriteTester by remember { mutableStateOf(false) } var spriteTesterView by remember { mutableStateOf("entry") } // "entry" or "testing" var dimId by remember { mutableStateOf("") } var monId by remember { mutableStateOf("") } var currentTestAnimation by remember { mutableStateOf(DigimonAnimationType.IDLE) } var testCharacterId by remember { mutableStateOf("") } + */ + /* // Create hardcoded character lists for each stage val rookieCharacters = listOf( APIBattleCharacter("AGUMON", "degimon_name_Dim012_003", "dim012_mon03", 0, 1, 1800, 1800, 2400.0f, 700.0f), @@ -1712,7 +1635,6 @@ fun BattlesScreen() { APIBattleCharacter("SLAYERDRAMON","degimon_name_dim129_mon15","dim129_mon15",3,1,4800, 4800, 6300.0f,1950.0f), APIBattleCharacter("BREAKDRAMON","degimon_name_dim129_mon17","dim129_mon17",3,2,6000, 6000, 4000.0f,1980.0f) ) - // Get the appropriate character list based on current stage val characterList = when (currentStage.lowercase()) { "rookie" -> rookieCharacters @@ -1721,9 +1643,7 @@ fun BattlesScreen() { "mega" -> megaCharacters else -> rookieCharacters } - - // Determine if player can battle based on stage (derived from activeUserCharacter) - val canBattle = activeUserCharacter?.stage?.let { it >= 2 } ?: false + */ // Get the appropriate battle type based on player's stage (derived from activeUserCharacter) val playerBattleType = activeUserCharacter?.stage?.let { stage -> @@ -1735,13 +1655,15 @@ fun BattlesScreen() { else -> null } } + + // Determine if player can battle based on stage (derived from activeUserCharacter) + val canBattle = activeUserCharacter?.stage?.let { it >= 2 } ?: false // Load opponents automatically based on player's stage // Only load if authenticated and character is ready LaunchedEffect(activeUserCharacter, isAuthenticated) { // Wait for authentication to complete before loading opponents if (!isAuthenticated) { - println("BATTLESCREEN: Skipping opponent load - not authenticated yet") return@LaunchedEffect } @@ -1754,7 +1676,6 @@ fun BattlesScreen() { // Create a new list to trigger UI recomposition opponentsList = ArrayList(opponents.opponentsList) println("BATTLESCREEN: Loaded ${opponents.opponentsList.size} opponents from API") - println("BATTLESCREEN: Total opponents in list: ${opponentsList.size}") } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1787,16 +1708,16 @@ fun BattlesScreen() { } if (token != null && token.isNotEmpty()) { - // Check if we've already successfully processed this token + // Check if we've already processed this token (either successfully or currently processing) if (!processedTokens.contains(token)) { - println("BATTLESCREEN: Received token from URI: $token (URI: $uri)") + // Mark token as being processed IMMEDIATELY to prevent duplicate API calls + processedTokens = processedTokens + token + println("BATTLESCREEN: Received token from URI: $token (URI: $uri) - marking as processing") // Exchange token with battle server RetrofitHelper().authenticate(context, token) { response -> if (response.success) { - // Only mark as processed after successful authentication - processedTokens = processedTokens + token - + // Token already marked as processed before API call, just extract userId // Extract userId from response val extractedUserId = response.userInfo?.userId?.toLongOrNull() kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { @@ -1812,7 +1733,35 @@ fun BattlesScreen() { } } else { println("BATTLESCREEN: Authentication failed: ${response.message}") - // Don't mark as processed on failure - allow retry with a new token + // If it's an "Invalid user nonce" error, the token was already used - keep it marked to prevent retries + if (response.message?.contains("Invalid user nonce") == true || response.message?.contains("nonce") == true) { + println("BATTLESCREEN: Token was already used (Invalid user nonce), keeping it marked to prevent retries") + // Token already marked as processed, just handle the error + // Clear authentication state and open login page + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + battleAuthContainer.authRepository.logout() + } + kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { + isAuthenticated = false + isCheckingAuth = false + // Small delay to ensure state is updated + kotlinx.coroutines.delay(100) + // Open auth URL to get a fresh token + val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" + val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) + try { + context.startActivity(authIntent) + println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl") + } catch (e: Exception) { + println("BATTLESCREEN: Failed to open auth URL: ${e.message}") + 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") + processedTokens = processedTokens - token + } // Show toast on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { android.widget.Toast.makeText(context, "Authentication failed: ${response.message}", android.widget.Toast.LENGTH_SHORT).show() @@ -1820,7 +1769,7 @@ fun BattlesScreen() { } } } else { - println("BATTLESCREEN: Token already successfully processed, skipping: $token") + println("BATTLESCREEN: Token already processed (or currently processing), skipping: $token") } } else { println("BATTLESCREEN: No token found in URI: $uri (checked 'c' and 'token' parameters)") @@ -2012,10 +1961,8 @@ fun BattlesScreen() { // Only check if permission is granted LaunchedEffect(hasStoragePermission) { if (hasStoragePermission) { - println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...") val spriteFileManager = SpriteFileManager(context) if (spriteFileManager.checkSpriteFilesExist()) { - println("BATTLESCREEN: Sprite files exist in external storage") } else { println("BATTLESCREEN: Sprite files not found in external storage") } @@ -2042,16 +1989,18 @@ fun BattlesScreen() { */ val activeChar = database.userCharacterDao().getActiveCharacter() - println("BATTLESCREEN: getActiveCharacter() returned: $activeChar") + //println("BATTLESCREEN: getActiveCharacter() returned: $activeChar") if (activeChar != null) { // Get the character data using the charId from activeChar val characterData = database.characterDao().getCharacterInfo(activeChar.charId) + /* println("BATTLESCREEN: CharacterData from getCharacterInfo:") println(" - cardId: ${characterData.cardId}") println(" - charId: ${characterData.charId}") println(" - stage: ${characterData.stage}") println(" - attribute: ${characterData.attribute}") + */ // The cardId from getCharacterInfo is already the correct card ID we need! val cardId = characterData.cardId @@ -2079,7 +2028,8 @@ fun BattlesScreen() { activeCardId = formattedCardId activeCharacter = playerCharacter // Set the active character for battle } - + + /* println("BATTLESCREEN: Loaded active character from database:") println(" - UserCharacter ID: ${activeChar.id}") println(" - CharId: ${activeChar.charId}") @@ -2090,6 +2040,7 @@ fun BattlesScreen() { println(" - Formatted as: $activeCardId") println(" - Can battle: ${activeChar.stage >= 2}") println(" - Battle type: ${when (activeChar.stage) { 2 -> "rookie"; 3 -> "champion"; 4 -> "ultimate"; 5 -> "mega"; else -> "none" }}") + */ } else { println("BATTLESCREEN: No active character found in database") withContext(Dispatchers.Main) { @@ -2103,7 +2054,6 @@ fun BattlesScreen() { } } - val backButton = @Composable { Button( onClick = { currentView = "main" } @@ -2112,200 +2062,6 @@ fun BattlesScreen() { } } - val characterDropdown = @Composable { currentStage: String -> - // Get the appropriate character list based on the passed currentStage parameter - val characterListForStage = when (currentStage.lowercase()) { - "rookie" -> rookieCharacters - "champion" -> championCharacters - "ultimate" -> ultimateCharacters - "mega" -> megaCharacters - else -> rookieCharacters - } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - OutlinedTextField( - value = selectedStage.ifEmpty { "Select Character" }, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.menuAnchor() - ) - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - characterListForStage.forEach { character -> - DropdownMenuItem( - text = { Text(character.name) }, - onClick = { - selectedStage = character.name - activeCharacter = character - expanded = false - println("Selected character: ${character.name}") - } - ) - } - } - } - } - - val spriteTesterEntry = @Composable { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(16.dp) - ) { - Text("Sprite Animation Tester", fontSize = 20.sp, fontWeight = FontWeight.Bold) - - Spacer(modifier = Modifier.height(16.dp)) - - // DIM ID input - OutlinedTextField( - value = dimId, - onValueChange = { dimId = it }, - label = { Text("DIM ID (e.g., 012)") }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Mon ID input - OutlinedTextField( - value = monId, - onValueChange = { monId = it }, - label = { Text("Mon ID (e.g., 03)") }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Load sprite button - Button( - onClick = { - if (dimId.isNotEmpty() && monId.isNotEmpty()) { - testCharacterId = "dim${dimId}_mon${monId}" - println("Testing sprite for: $testCharacterId") - spriteTesterView = "testing" - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Load Sprite") - } - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { showSpriteTester = false }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Back to Main") - } - } - } - - val spriteTesterTesting = @Composable { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(16.dp) - ) { - Text("Sprite Animation Testing", fontSize = 20.sp, fontWeight = FontWeight.Bold) - Text("Character: $testCharacterId", fontSize = 14.sp, color = Color.Gray) - - Spacer(modifier = Modifier.height(16.dp)) - - // Display sprite - AnimatedSpriteImage( - characterId = testCharacterId, - animationType = currentTestAnimation, - modifier = Modifier.size(120.dp), - contentScale = ContentScale.Fit, - reloadMappings = false, - animationOffset = 0L // No offset for sprite tester - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Animation buttons in a grid - Text("Animation Buttons:", fontSize = 16.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - - val animationTypes = listOf( - DigimonAnimationType.IDLE to "IDLE", - DigimonAnimationType.IDLE2 to "IDLE2", - DigimonAnimationType.WALK to "WALK", - DigimonAnimationType.WALK2 to "WALK2", - DigimonAnimationType.RUN to "RUN", - DigimonAnimationType.RUN2 to "RUN2", - DigimonAnimationType.WORKOUT to "WORKOUT", - DigimonAnimationType.WORKOUT2 to "WORKOUT2", - DigimonAnimationType.HAPPY to "HAPPY", - DigimonAnimationType.SLEEP to "SLEEP", - DigimonAnimationType.ATTACK to "ATTACK", - DigimonAnimationType.FLEE to "FLEE" - ) - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - // Create rows of 3 buttons each - animationTypes.chunked(3).forEach { row -> - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - row.forEach { (animationType, label) -> - Button( - onClick = { - currentTestAnimation = animationType - println("Switched to animation: $label") - }, - colors = ButtonDefaults.buttonColors( - containerColor = if (currentTestAnimation == animationType) Color.Blue else Color.Gray - ), - modifier = Modifier.weight(1f), - contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 4.dp, horizontal = 2.dp) - ) { - Text(label, fontSize = 10.sp) - } - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Navigation buttons - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = { spriteTesterView = "entry" }, - modifier = Modifier.weight(1f) - ) { - Text("Back to Entry") - } - - Button( - onClick = { - showSpriteTester = false - spriteTesterView = "entry" - testCharacterId = "" - dimId = "" - monId = "" - }, - modifier = Modifier.weight(1f) - ) { - Text("Back to Main") - } - } - } - } - Scaffold ( topBar = { // Only show TopBanner when not in battle mode @@ -2356,21 +2112,15 @@ fun BattlesScreen() { ) } } - } else if (showSpriteTester) { - when (spriteTesterView) { - "entry" -> spriteTesterEntry() - "testing" -> spriteTesterTesting() - else -> spriteTesterEntry() - } - } else { + } + else { Column( horizontalAlignment = Alignment.CenterHorizontally ) { // Show active character info activeUserCharacter?.let { character -> - Text("Active Character: ${character.id}", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text("Active Digimon:") Text("Stage: ${character.stage}") - Text("Age: ${character.ageInDays} days") activeCardId?.let { cardId -> Text("Digimon ID: $cardId", fontSize = 14.sp, color = Color.Blue, fontWeight = FontWeight.Bold) } @@ -2379,11 +2129,10 @@ fun BattlesScreen() { if (canBattle) { Text("Available Opponents:", fontSize = 18.sp, fontWeight = FontWeight.Bold) - Text("Debug: opponentsList.size = ${opponentsList.size}", fontSize = 12.sp, color = Color.Gray) + //Text("Debug: opponentsList.size = ${opponentsList.size}", fontSize = 12.sp, color = Color.Gray) Spacer(modifier = Modifier.height(8.dp)) if (opponentsList.isNotEmpty()) { - println("BATTLESCREEN: UI - Showing ${opponentsList.size} opponents") // Show scrollable list of opponents LazyColumn( modifier = Modifier @@ -2443,30 +2192,6 @@ fun BattlesScreen() { } ?: run { Text("No active character found in database", fontSize = 16.sp, color = Color.Red) } - - /* - Button( - onClick = { - showSpriteTester = true - spriteTesterView = "entry" - testCharacterId = "" - dimId = "" - monId = "" - currentTestAnimation = DigimonAnimationType.IDLE - } - ) { - Text("Sprite Animation Tester") - } - Button( - onClick = { - val spriteFileManager = SpriteFileManager(context) - spriteFileManager.clearSpriteFiles() - println("Sprite files cleared!") - } - ) { - Text("Clear Sprite Files") - } - */ } } } @@ -2614,8 +2339,6 @@ fun AnimatedBattleBackground( val backgroundFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/BattleBg_0015_BattleBg_0012.png") if (backgroundFile.exists()) { backgroundBitmap = BitmapFactory.decodeFile(backgroundFile.absolutePath) - println("Successfully loaded battle background: ${backgroundFile.absolutePath}") - println("DEBUG: Image dimensions = ${backgroundBitmap?.width}x${backgroundBitmap?.height} pixels") } else { println("Battle background file not found: ${backgroundFile.absolutePath}") } @@ -2634,7 +2357,6 @@ fun AnimatedBattleBackground( // Create perfect loop by resetting when one full screen width has moved if (xOffset <= -screenWidth.value) { xOffset = 0f - println("DEBUG: Background loop reset at xOffset = ${xOffset}") } } } From d1908d629ac2c356dabf3a49ac25ad50f3de8e17 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 16:52:07 -0500 Subject: [PATCH 78/89] Fixed logout when swapping from screen orientation. --- .../vbhelper/screens/BattlesScreen.kt | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) 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 824343e..c171f73 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 @@ -20,6 +20,7 @@ import androidx.compose.material3.Button import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.draw.scale @@ -1584,7 +1585,8 @@ fun BattlesScreen() { var isCheckingAuth by remember { mutableStateOf(true) } var userId by remember { mutableStateOf(null) } // Track processed tokens to prevent duplicate API calls - var processedTokens by remember { mutableStateOf>(emptySet()) } + // Use rememberSaveable to persist across configuration changes (screen rotation) + var processedTokens by rememberSaveable { mutableStateOf>(emptySet()) } var opponentsList by remember { mutableStateOf(ArrayList()) } @@ -1790,6 +1792,16 @@ fun BattlesScreen() { userId = storedUserId } + // If we have a stored token, set authenticated state optimistically FIRST (before checking deep links) + // This must happen immediately to prevent UI from showing "Checking authentication" + if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { + // Set authenticated state immediately to prevent redirect on rotation and show UI + isAuthenticated = true + isCheckingAuth = false + userId = storedUserId + println("BATTLESCREEN: Restored authentication state from storage (userId: $storedUserId) - isCheckingAuth set to false") + } + // Only check for token in intent if it's a fresh deep link (ACTION_VIEW intent) // This prevents processing stale tokens from previous sessions val activity = context as? ComponentActivity @@ -1805,9 +1817,10 @@ fun BattlesScreen() { } } - // If we have a stored token, validate it with the server + // If we have a stored token, validate it with the server in the background if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { - println("BATTLESCREEN: Validating stored token with server...") + // State already set above, now validate in background + println("BATTLESCREEN: Validating stored token with server in background...") RetrofitHelper().authenticate(context, storedToken) { response -> // Update UI on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { @@ -1825,17 +1838,31 @@ fun BattlesScreen() { userId = extractedUserId } else { println("BATTLESCREEN: Token validation failed: ${response.message}") - // Clear authentication state - kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { - authRepository.logout() + // Check if it's a critical error that requires re-authentication + // (e.g., "Invalid user nonce" means token was already used) + val isCriticalError = response.message?.contains("Invalid user nonce") == true || + response.message?.contains("nonce") == true || + response.message?.contains("invalid") == true || + response.message?.contains("expired") == true + + if (isCriticalError) { + // Critical error - token is invalid, need to re-authenticate + println("BATTLESCREEN: Critical authentication error, clearing state and redirecting") + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + authRepository.logout() + } + isAuthenticated = false + isCheckingAuth = false + // Open auth URL + val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" + val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) + context.startActivity(authIntent) + println("BATTLESCREEN: Opened auth URL after critical validation failure: $authUrl") + } else { + // Non-critical error (e.g., network issue) - keep authenticated state + // This prevents redirect on rotation if validation fails due to network + println("BATTLESCREEN: Non-critical validation error, keeping authenticated state") } - isAuthenticated = false - isCheckingAuth = false - // Open auth URL - val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" - val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) - context.startActivity(authIntent) - println("BATTLESCREEN: Opened auth URL after validation failure: $authUrl") } } } @@ -2067,7 +2094,7 @@ fun BattlesScreen() { // Only show TopBanner when not in battle mode if (currentView != "battle-main" && currentView != "battle-results") { TopBanner( - text = "Online battles" + text = "Online Battles" ) } } From c4450296db2dd3c541695d3b6677042f0a9bedaa Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 17:22:25 -0500 Subject: [PATCH 79/89] Removed debugging logging. --- .../vbhelper/battle/AnimatedSpriteImage.kt | 5 +-- .../vbhelper/battle/ArenaBattleSystem.kt | 35 ------------------- .../vbhelper/battle/AttackSpriteManager.kt | 13 ------- .../vbhelper/battle/DigimonAnimationState.kt | 8 ++--- .../vbhelper/battle/HitEffectComposables.kt | 1 - .../vbhelper/battle/HitEffectSpriteManager.kt | 2 -- .../battle/IndividualSpriteManager.kt | 2 -- .../vbhelper/battle/RetrofitHelper.kt | 6 ---- .../nacabaro/vbhelper/battle/SpriteImage.kt | 2 -- .../vbhelper/components/AttackSpriteImage.kt | 1 - .../vbhelper/screens/BattlesScreen.kt | 35 ------------------- 11 files changed, 4 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt index ae872bf..f75b935 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AnimatedSpriteImage.kt @@ -63,14 +63,11 @@ fun AnimatedSpriteImage( // Update sprite when animation state changes LaunchedEffect(animationStateMachine.currentFrameNumber) { val frameNumber = animationStateMachine.getCurrentFrame() - - println("Loading animated sprite frame: $frameNumber for character: $characterId") + bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber) if (bitmap == null) { println("Failed to load animated sprite frame: $frameNumber for character: $characterId") - } else { - println("Successfully loaded animated sprite frame: $frameNumber for character: $characterId") } } 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 61073fe..728c17d 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 @@ -112,7 +112,6 @@ class ArenaBattleSystem { val opponentAttackIsHit: Boolean get() = _opponentAttackIsHit fun startPlayerAttack() { - Log.d(TAG, "Starting player attack") _attackPhase = 1 _attackProgress = 0f _isPlayerAttacking = true @@ -121,7 +120,6 @@ class ArenaBattleSystem { } fun startOpponentAttack() { - Log.d(TAG, "Starting opponent attack") _attackPhase = 3 _attackProgress = 0f _isPlayerAttacking = false @@ -131,7 +129,6 @@ class ArenaBattleSystem { fun advanceAttackPhase() { _attackPhase++ _attackProgress = 0f - Log.d(TAG, "Advanced to attack phase: $_attackPhase") } fun setAttackProgress(progress: Float) { @@ -144,12 +141,10 @@ class ArenaBattleSystem { fun switchToView(view: Int) { _currentView = view - Log.d(TAG, "Switched to view: $view") } fun enableAttackButton() { _isAttackButtonEnabled = true - Log.d(TAG, "Attack button enabled") } fun applyDamage(isPlayer: Boolean, damage: Float) { @@ -158,19 +153,16 @@ class ArenaBattleSystem { } else { _opponentHP = (_opponentHP - damage).coerceAtLeast(0f) } - Log.d(TAG, "Applied damage: ${if (isPlayer) "player" else "opponent"} -$damage") } fun updateHPFromAPI(playerHP: Float, opponentHP: Float) { _playerHP = playerHP _opponentHP = opponentHP - Log.d(TAG, "Updated HP from API: Player=$playerHP, Opponent=$opponentHP") } fun initializeHP(playerMaxHP: Float, opponentMaxHP: Float) { _playerHP = playerMaxHP _opponentHP = opponentMaxHP - Log.d(TAG, "Initialized HP: Player=$playerMaxHP, Opponent=$opponentMaxHP") } fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) { @@ -180,7 +172,6 @@ class ArenaBattleSystem { if (opponentDamage > 0f) { applyDamage(false, opponentDamage) } - Log.d(TAG, "Completed attack animation with damage: Player=$playerDamage, Opponent=$opponentDamage") } fun resetAttackState() { @@ -209,7 +200,6 @@ class ArenaBattleSystem { _shouldCounterAttack = false _counterAttackIsHit = false _opponentAttackIsHit = false - Log.d(TAG, "Reset attack state") } fun checkBattleOver(): Boolean { @@ -218,7 +208,6 @@ class ArenaBattleSystem { fun endBattle() { _isBattleOver = true - Log.d(TAG, "Battle ended") } fun updateCritBarProgress(progress: Int) { @@ -231,7 +220,6 @@ class ArenaBattleSystem { _isDodging = true _dodgeProgress = 0f _dodgeDirection = 1f // Start moving up - Log.d(TAG, "Started dodge animation") } fun setDodgeProgress(progress: Float) { @@ -245,14 +233,12 @@ class ArenaBattleSystem { fun endDodge() { _isDodging = false _dodgeProgress = 0f - Log.d(TAG, "Ended dodge animation") } // Hit animation methods fun startHit() { _isHit = true _hitProgress = 0f - Log.d(TAG, "Started hit animation") } fun setHitProgress(progress: Float) { @@ -262,7 +248,6 @@ class ArenaBattleSystem { fun endHit() { _isHit = false _hitProgress = 0f - Log.d(TAG, "Ended hit animation") } // Player-specific dodge methods @@ -270,13 +255,11 @@ class ArenaBattleSystem { _isPlayerDodging = true _playerDodgeProgress = 0f _playerDodgeDirection = 1f - Log.d(TAG, "Started player dodge animation") } fun endPlayerDodge() { _isPlayerDodging = false _playerDodgeProgress = 0f - Log.d(TAG, "Ended player dodge animation") } fun setPlayerDodgeProgress(progress: Float) { @@ -292,13 +275,11 @@ class ArenaBattleSystem { _isOpponentDodging = true _opponentDodgeProgress = 0f _opponentDodgeDirection = 1f - Log.d(TAG, "Started opponent dodge animation") } fun endOpponentDodge() { _isOpponentDodging = false _opponentDodgeProgress = 0f - Log.d(TAG, "Ended opponent dodge animation") } fun setOpponentDodgeProgress(progress: Float) { @@ -313,67 +294,55 @@ class ArenaBattleSystem { fun startPlayerHit() { _isPlayerHit = true _hitProgress = 0f - Log.d(TAG, "Started player hit animation") } fun startPlayerHitDelayed() { _isPlayerHitDelayed = true - Log.d(TAG, "Started delayed player hit animation") } fun endPlayerHit() { _isPlayerHit = false _hitProgress = 0f - Log.d(TAG, "Ended player hit animation") } fun endPlayerHitDelayed() { _isPlayerHitDelayed = false - Log.d(TAG, "Ended delayed player hit animation") } // Opponent-specific hit methods fun startOpponentHit() { _isOpponentHit = true _hitProgress = 0f - Log.d(TAG, "Started opponent hit animation") } fun startOpponentHitDelayed() { _isOpponentHitDelayed = true - Log.d(TAG, "Started delayed opponent hit animation") } fun endOpponentHit() { _isOpponentHit = false _hitProgress = 0f - Log.d(TAG, "Ended opponent hit animation") } fun endOpponentHitDelayed() { _isOpponentHitDelayed = false - Log.d(TAG, "Ended delayed opponent hit animation") } // Delayed shake methods fun startPlayerShakeDelayed() { _isPlayerShakeDelayed = true - Log.d(TAG, "Started delayed player shake animation") } fun endPlayerShakeDelayed() { _isPlayerShakeDelayed = false - Log.d(TAG, "Ended delayed player shake animation") } fun startOpponentShakeDelayed() { _isOpponentShakeDelayed = true - Log.d(TAG, "Started delayed opponent shake animation") } fun endOpponentShakeDelayed() { _isOpponentShakeDelayed = false - Log.d(TAG, "Ended delayed opponent shake animation") } // Combined method to handle attack result @@ -386,7 +355,6 @@ class ArenaBattleSystem { // Player attack missed - opponent dodges startOpponentDodge() } - Log.d(TAG, "Handled player attack result: ${if (isHit) "HIT" else "DODGE"}") } // Method to handle opponent attack result @@ -399,14 +367,12 @@ class ArenaBattleSystem { // Opponent attack missed - player dodges startPlayerDodge() } - Log.d(TAG, "Handled opponent attack result: ${if (isHit) "HIT" else "DODGE"}") } // Counter-attack methods fun setupCounterAttack(isHit: Boolean) { _shouldCounterAttack = true _counterAttackIsHit = isHit - Log.d(TAG, "Setup counter-attack: ${if (isHit) "HIT" else "DODGE"}, isHit=$isHit") } fun startCounterAttack() { @@ -415,6 +381,5 @@ class ArenaBattleSystem { _isPlayerAttacking = false _currentView = 1 _opponentAttackIsHit = _counterAttackIsHit - Log.d(TAG, "Started counter-attack with opponentAttackIsHit=$_opponentAttackIsHit, counterAttackIsHit=$_counterAttackIsHit") } } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt index 30d144b..e47db7e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt @@ -43,15 +43,12 @@ class AttackSpriteManager(private val context: Context) { try { // Get character data val characterData = getCharacterData(characterId) ?: return null - println("AttackSpriteManager: Got character data: $characterData") - // Determine which attack file to use val attackFileName = if (isLarge) { characterData.laugeFileName } else { characterData.smalefilename } - println("AttackSpriteManager: Attack filename = $attackFileName") // Skip if no attack file if (attackFileName == "0") { @@ -61,12 +58,9 @@ class AttackSpriteManager(private val context: Context) { // Load the attack sprite from external storage val attackFile = File(getAttackTexturesBaseDir(), "$attackFileName.png") - println("AttackSpriteManager: Attack file path = ${attackFile.absolutePath}") - println("AttackSpriteManager: Attack file exists = ${attackFile.exists()}") return if (attackFile.exists()) { val bitmap = BitmapFactory.decodeFile(attackFile.absolutePath) - println("AttackSpriteManager: Successfully loaded bitmap = ${bitmap != null}") bitmap } else { println("AttackSpriteManager: Attack file does not exist") @@ -80,10 +74,8 @@ class AttackSpriteManager(private val context: Context) { } private fun getCharacterData(characterId: String): CharacterData? { - println("AttackSpriteManager: Getting character data for characterId=$characterId") // Check cache first if (characterDataCache.containsKey(characterId)) { - println("AttackSpriteManager: Found character data in cache") return characterDataCache[characterId] } @@ -91,8 +83,6 @@ class AttackSpriteManager(private val context: Context) { // Load character data from JSON file in external storage val externalDir = android.os.Environment.getExternalStorageDirectory() val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json") - println("AttackSpriteManager: Character data file path = ${characterDataFile.absolutePath}") - println("AttackSpriteManager: Character data file exists = ${characterDataFile.exists()}") if (!characterDataFile.exists()) { println("AttackSpriteManager: Character data file does not exist, using default data") @@ -109,7 +99,6 @@ class AttackSpriteManager(private val context: Context) { } val jsonContent = characterDataFile.readText() - println("AttackSpriteManager: JSON content length = ${jsonContent.length}") // Parse the JSON response val response = gson.fromJson(jsonContent, CharacterDataResponse::class.java) @@ -136,7 +125,6 @@ class AttackSpriteManager(private val context: Context) { ) characterDataCache[characterId] = characterData - println("AttackSpriteManager: Found character data: $characterData") return characterData } } @@ -152,7 +140,6 @@ class AttackSpriteManager(private val context: Context) { ) characterDataCache[characterId] = characterData - println("AttackSpriteManager: Created default character data: $characterData") return characterData } catch (e: Exception) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt index d23ea11..c2ee41e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt @@ -79,11 +79,13 @@ class DigimonAnimationStateMachine( DigimonAnimationType.ATTACK to 650L, DigimonAnimationType.FLEE to 150L ) - + + /* init { println("Initialized DigimonAnimationStateMachine for character: $characterId with frame offset: $initialFrameOffset, timing offset: $timingOffset") println("Available animation types: ${animationTypeToFrames.keys}") } + */ suspend fun playAnimation(animationType: DigimonAnimationType) { if (currentAnimation == animationType && isPlaying) { @@ -96,8 +98,6 @@ class DigimonAnimationStateMachine( val frameNumbers = animationTypeToFrames[animationType] ?: listOf(1) val duration = animationDurations[animationType] ?: 100L - println("Playing animation: $animationType with frames: $frameNumbers") - // For non-looping animations like ATTACK, play once and return to IDLE if (animationType == DigimonAnimationType.ATTACK) { currentFrameNumber = frameNumbers.firstOrNull() ?: 1 @@ -130,8 +130,6 @@ class DigimonAnimationStateMachine( // Combine frames for cycling idle animation val combinedFrames = (idleFrames + idle2Frames).distinct() - println("Playing idle animation with frames: $combinedFrames, starting at offset: $initialFrameOffset, timing offset: $timingOffset") - val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L // Apply initial timing offset diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt index 1f3210e..e4a3383 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt @@ -39,7 +39,6 @@ fun HitEffectOverlay( LaunchedEffect(isVisible) { if (isVisible) { - println("DEBUG: Starting hit effect animation") // Add delay before starting hit effect animation delay(400) // Increased from 200ms to 400ms delay before hit effect appears diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt index c33205c..fa116be 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -44,8 +44,6 @@ class HitEffectSpriteManager(private val context: Context) { return null } - println("Successfully loaded hit sprite: $spriteName.png (${bitmap.width}x${bitmap.height})") - // Cache the result spriteCache[cacheKey] = bitmap diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt index 56b664f..84c68a7 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt @@ -53,8 +53,6 @@ class IndividualSpriteManager(private val context: Context) { return null } - println("Successfully loaded sprite frame: $spriteFileName (${bitmap.width}x${bitmap.height})") - // Cache the result spriteCache[cacheKey] = bitmap 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 90836a3..91ba907 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 @@ -205,9 +205,6 @@ class RetrofitHelper { val service: AuthService = retrofit.create(AuthService::class.java) val request = AuthenticateRequest(userToken = token) - println("RetrofitHelper: Sending request to api/auth/validate with userToken: $token") - //println("RetrofitHelper: Request object: $request") - //println("RetrofitHelper: Request JSON will be: {\"userToken\": \"$token\"}") val call: Call = service.validate(request) call.enqueue(object : Callback { @@ -218,13 +215,10 @@ class RetrofitHelper { } override fun onResponse(call: Call, response: Response) { - println("RetrofitHelper: Validate API response received - Code: ${response.code()}") - println("RetrofitHelper: Response body: ${response.body()}") if (response.isSuccessful) { val authResponse: AuthenticateResponse? = response.body() if (authResponse != null) { - println("RetrofitHelper: Validation successful: ${authResponse.success}, message: ${authResponse.message}") callback(authResponse) } else { println("RetrofitHelper: Validation failed: Invalid response body") diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt index f7d4430..5e1c141 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt @@ -24,8 +24,6 @@ fun SpriteImage( bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber) if (bitmap == null) { println("Failed to load sprite frame: $frameNumber for character: $characterId") - } else { - println("Successfully loaded sprite frame: $frameNumber for character: $characterId") } } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt index 51f4ed3..61f2948 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt @@ -30,7 +30,6 @@ fun AttackSpriteImage( val loadedBitmap = withContext(Dispatchers.IO) { attackSpriteManager.getAttackSprite(characterId, isLarge) } - println("AttackSpriteImage: Loaded bitmap = ${loadedBitmap != null}") bitmap = loadedBitmap } } 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 c171f73..9528c36 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 @@ -1051,39 +1051,28 @@ fun MiddleBattleView( when (apiResult.state) { 1 -> { // Match is still ongoing - update HP and continue - println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Set pending damage based on API result if (apiResult.playerAttackDamage > 0) { // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage battleSystem.setAttackHitState(true) // Also check if enemy counter-attacks and hits if (apiResult.opponentAttackDamage > 0) { - println("Enemy counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), apiResult.playerAttackDamage.toFloat()) // Both take damage } } else { // Player attack missed - enemy counter-attacks - println("Player attack missed! Enemy counter-attacks") battleSystem.setAttackHitState(false) // Set up counter-attack - determine if it hits based on API result val counterAttackHits = apiResult.opponentAttackDamage > 0 - println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") - println("Full API response: status=${apiResult.status}, state=${apiResult.state}, playerAttackHit=${apiResult.playerAttackHit}, playerAttackDamage=${apiResult.playerAttackDamage}, opponentAttackDamage=${apiResult.opponentAttackDamage}, playerHP=${apiResult.playerHP}, opponentHP=${apiResult.opponentHP}") - println("DEBUG: Using playerAttackDamage > 0 instead of playerAttackHit for hit detection") // Use opponentAttackDamage to determine counter-attack hit val finalCounterAttackHits = counterAttackHits - println("Using opponentAttackDamage > 0 for counter-attack: $finalCounterAttackHits") if (finalCounterAttackHits) { - println("Counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage } else { - println("Counter-attack misses! Player dodges") onSetPendingDamage(0f, 0f) // No damage } battleSystem.setupCounterAttack(finalCounterAttackHits) @@ -1671,13 +1660,11 @@ fun BattlesScreen() { val currentCharacter = activeUserCharacter if (currentCharacter != null && canBattle && playerBattleType != null) { - println("BATTLESCREEN: Loading opponents for stage ${currentCharacter.stage}, battle type: $playerBattleType") try { RetrofitHelper().getOpponents(context, playerBattleType!!) { opponents -> try { // Create a new list to trigger UI recomposition opponentsList = ArrayList(opponents.opponentsList) - println("BATTLESCREEN: Loaded ${opponents.opponentsList.size} opponents from API") } catch (e: Exception) { Log.d(TAG, "Error processing opponents data: ${e.message}") e.printStackTrace() @@ -1714,7 +1701,6 @@ fun BattlesScreen() { if (!processedTokens.contains(token)) { // Mark token as being processed IMMEDIATELY to prevent duplicate API calls processedTokens = processedTokens + token - println("BATTLESCREEN: Received token from URI: $token (URI: $uri) - marking as processing") // Exchange token with battle server RetrofitHelper().authenticate(context, token) { response -> @@ -1770,8 +1756,6 @@ fun BattlesScreen() { } } } - } else { - println("BATTLESCREEN: Token already processed (or currently processing), skipping: $token") } } else { println("BATTLESCREEN: No token found in URI: $uri (checked 'c' and 'token' parameters)") @@ -1785,7 +1769,6 @@ fun BattlesScreen() { val localAuthState = authRepository.isAuthenticated.first() val storedToken = authRepository.authToken.first() val storedUserId = authRepository.userId.first() - println("BATTLESCREEN: Local authentication status - isAuthenticated: $localAuthState, hasToken: ${storedToken != null}, userId: $storedUserId") // Load stored userId if available if (storedUserId != null) { @@ -1799,7 +1782,6 @@ fun BattlesScreen() { isAuthenticated = true isCheckingAuth = false userId = storedUserId - println("BATTLESCREEN: Restored authentication state from storage (userId: $storedUserId) - isCheckingAuth set to false") } // Only check for token in intent if it's a fresh deep link (ACTION_VIEW intent) @@ -1808,9 +1790,7 @@ fun BattlesScreen() { val intent = activity?.intent if (intent?.action == Intent.ACTION_VIEW) { intent.data?.let { uri -> - println("BATTLESCREEN: Found ACTION_VIEW intent with URI: $uri") if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { - println("BATTLESCREEN: Token found in fresh deep link, processing...") handleTokenFromUri(uri) return@LaunchedEffect // Don't open auth URL if we're processing a token } @@ -1820,7 +1800,6 @@ fun BattlesScreen() { // If we have a stored token, validate it with the server in the background if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { // State already set above, now validate in background - println("BATTLESCREEN: Validating stored token with server in background...") RetrofitHelper().authenticate(context, storedToken) { response -> // Update UI on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { @@ -1832,7 +1811,6 @@ fun BattlesScreen() { authRepository.setAuthenticated(true, storedToken, extractedUserId) } } - println("BATTLESCREEN: Token validation successful, userId: $extractedUserId") isAuthenticated = true isCheckingAuth = false userId = extractedUserId @@ -1897,23 +1875,18 @@ fun BattlesScreen() { if (intent?.action == Intent.ACTION_VIEW) { val uri = intent.data if (uri != null) { - println("BATTLESCREEN: Checking ACTION_VIEW intent data - URI: $uri, scheme: ${uri.scheme}, host: ${uri.host}, path: ${uri.path}") - println("BATTLESCREEN: All query parameters: ${uri.queryParameterNames}") // Handle vbhelper://auth?token= or vbhelper://auth?c= deep link if (uri.scheme == "vbhelper" && uri.host == "auth") { - println("BATTLESCREEN: Detected vbhelper://auth deep link") handleTokenFromUri(uri) } // Handle http://localhost:8080/authenticate?c= redirect else if ((uri.scheme == "http" || uri.scheme == "https") && (uri.host == "localhost" || uri.host == "127.0.0.1" || uri.host?.contains("8080") == true)) { - println("BATTLESCREEN: Detected localhost redirect, checking for token") handleTokenFromUri(uri) } // Also check if there's a 'c' or 'token' parameter in any URL else if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { - println("BATTLESCREEN: Found token parameter (c or token) in URI, attempting to authenticate") handleTokenFromUri(uri) } else { println("BATTLESCREEN: URI found but no token parameter detected") @@ -1938,9 +1911,7 @@ fun BattlesScreen() { val intent = activity?.intent if (intent?.action == Intent.ACTION_VIEW) { intent.data?.let { uri -> - println("BATTLESCREEN: Activity resumed with ACTION_VIEW intent, checking for token - URI: $uri") if (uri.getQueryParameter("c") != null || uri.getQueryParameter("token") != null) { - println("BATTLESCREEN: Found token in fresh deep link on resume") handleTokenFromUri(uri) } } @@ -2204,7 +2175,6 @@ fun BattlesScreen() { } } } else { - println("BATTLESCREEN: UI - No opponents in list, showing message") Text("No opponents available for your stage", fontSize = 16.sp, color = Color(0xFFFFA500), // Orange color @@ -2288,7 +2258,6 @@ fun BattlesScreen() { selectedOpponent?.name ?: "Opponent", opponentStage ) { cleanupResult -> - println("Cleanup call completed") } } } @@ -2460,7 +2429,6 @@ fun MultiLayerAnimatedBattleBackground( LaunchedEffect(Unit) { screenWidth = with(density) { configuration.screenWidthDp.dp } screenHeight = with(density) { configuration.screenHeightDp.dp } - println("DEBUG: Multi-layer screen dimensions = ${screenWidth.value}x${screenHeight.value}dp") } // Load all three background layers from external storage @@ -2473,7 +2441,6 @@ fun MultiLayerAnimatedBattleBackground( val backLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.backLayer}") if (backLayerFile.exists()) { backLayerBitmap = BitmapFactory.decodeFile(backLayerFile.absolutePath) - println("Successfully loaded back layer background (Set ${backgroundSetIndex + 1}): ${backLayerFile.absolutePath}") } else { println("Back layer background file not found: ${backLayerFile.absolutePath}") } @@ -2482,7 +2449,6 @@ fun MultiLayerAnimatedBattleBackground( val middleLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.middleLayer}") if (middleLayerFile.exists()) { middleLayerBitmap = BitmapFactory.decodeFile(middleLayerFile.absolutePath) - println("Successfully loaded middle layer background (Set ${backgroundSetIndex + 1}): ${middleLayerFile.absolutePath}") } else { println("Middle layer background file not found: ${middleLayerFile.absolutePath}") } @@ -2491,7 +2457,6 @@ fun MultiLayerAnimatedBattleBackground( val frontLayerFile = File(externalDir, "VBHelper/battle_sprites/extracted_battlebgs/${selectedSet.frontLayer}") if (frontLayerFile.exists()) { frontLayerBitmap = BitmapFactory.decodeFile(frontLayerFile.absolutePath) - println("Successfully loaded front layer background (Set ${backgroundSetIndex + 1}): ${frontLayerFile.absolutePath}") } else { println("Front layer background file not found: ${frontLayerFile.absolutePath}") } From 54b29051966b1961f11182b4f3776851a138c359 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 17:25:57 -0500 Subject: [PATCH 80/89] Changed to use small attack sprites instead of large. --- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 9528c36..6b740d7 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 @@ -845,7 +845,7 @@ fun MiddleBattleView( AttackSpriteImage( characterId = opponentCharacter?.charaId ?: "dim011_mon01", - isLarge = true, + isLarge = false, modifier = Modifier .size(60.dp) .offset( @@ -932,7 +932,7 @@ fun MiddleBattleView( AttackSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", - isLarge = true, + isLarge = false, modifier = Modifier .size(60.dp) .offset( @@ -1284,7 +1284,7 @@ fun PlayerBattleView( if (!isTransitioning && !hidePlayerAttackSprite) { AttackSpriteImage( characterId = characterId, - isLarge = true, + isLarge = false, modifier = Modifier .size(60.dp) .offset( @@ -1458,7 +1458,7 @@ fun EnemyBattleView( if (!isTransitioning && !hideEnemyAttackSprite) { AttackSpriteImage( characterId = characterId, - isLarge = true, + isLarge = false, modifier = Modifier .size(60.dp) .offset( From c774fd1536f56d2a61bee05cc81c9e25ec1367b5 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 17:34:58 -0500 Subject: [PATCH 81/89] Adjusted auth to not force login every time app is reopened. --- .../vbhelper/components/AttackSpriteImage.kt | 2 +- .../vbhelper/screens/BattlesScreen.kt | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt index 61f2948..6bcf434 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt @@ -24,7 +24,7 @@ fun AttackSpriteImage( val context = LocalContext.current LaunchedEffect(characterId, isLarge) { - println("AttackSpriteImage: Loading attack sprite for characterId=$characterId, isLarge=$isLarge") + //println("AttackSpriteImage: Loading attack sprite for characterId=$characterId, isLarge=$isLarge") coroutineScope.launch { val attackSpriteManager = AttackSpriteManager(context) val loadedBitmap = withContext(Dispatchers.IO) { 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 6b740d7..da88280 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 @@ -1797,16 +1797,28 @@ fun BattlesScreen() { } } - // If we have a stored token, validate it with the server in the background - if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { - // State already set above, now validate in background + // If we have a stored token and userId, assume session is still active + // The single-use token can't be re-validated, but the server maintains a session after initial validation + // We'll only re-authenticate if API calls fail with authentication errors + if (localAuthState && storedToken != null && storedToken.isNotEmpty() && storedUserId != null) { + // Session appears to be active - user is already authenticated + // No need to re-validate the single-use token, just restore the session + println("BATTLESCREEN: Restoring active session (userId: $storedUserId)") + isAuthenticated = true + isCheckingAuth = false + userId = storedUserId + // Session is restored, no need to validate token again + } else if (localAuthState && storedToken != null && storedToken.isNotEmpty()) { + // We have a token but no userId - try to validate once to get userId + // This should only happen on first login or if userId was lost + println("BATTLESCREEN: Have token but no userId, validating once to get userId...") RetrofitHelper().authenticate(context, storedToken) { response -> // Update UI on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { if (response.success) { - val extractedUserId = response.userInfo?.userId?.toLongOrNull() ?: storedUserId - // Update stored userId if we got a new one - if (extractedUserId != null && extractedUserId != storedUserId) { + val extractedUserId = response.userInfo?.userId?.toLongOrNull() + // Update stored userId + if (extractedUserId != null) { kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { authRepository.setAuthenticated(true, storedToken, extractedUserId) } @@ -1814,10 +1826,10 @@ fun BattlesScreen() { isAuthenticated = true isCheckingAuth = false userId = extractedUserId + println("BATTLESCREEN: Got userId from validation: $extractedUserId") } else { println("BATTLESCREEN: Token validation failed: ${response.message}") // Check if it's a critical error that requires re-authentication - // (e.g., "Invalid user nonce" means token was already used) val isCriticalError = response.message?.contains("Invalid user nonce") == true || response.message?.contains("nonce") == true || response.message?.contains("invalid") == true || @@ -1838,8 +1850,9 @@ fun BattlesScreen() { println("BATTLESCREEN: Opened auth URL after critical validation failure: $authUrl") } else { // Non-critical error (e.g., network issue) - keep authenticated state - // This prevents redirect on rotation if validation fails due to network println("BATTLESCREEN: Non-critical validation error, keeping authenticated state") + isAuthenticated = true + isCheckingAuth = false } } } From eb82c2afc11d21b3610defc85a7d2c0b8e083fa6 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 17 Nov 2025 17:52:53 -0500 Subject: [PATCH 82/89] Added BGM to battles. --- .../vbhelper/screens/BattlesScreen.kt | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) 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 da88280..aac0482 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 @@ -49,6 +49,8 @@ import android.os.Build import android.provider.Settings import android.content.Intent import android.net.Uri +import android.media.MediaPlayer +import android.os.Environment //import androidx.compose.animation.core.animateFloatAsState //import androidx.compose.animation.core.tween import kotlinx.coroutines.delay @@ -231,6 +233,9 @@ fun BattleScreen( val battleSystem = remember { ArenaBattleSystem() } val coroutineScope = rememberCoroutineScope() + // Background music MediaPlayer + var mediaPlayer by remember { mutableStateOf(null) } + // Initialize HP when battle starts LaunchedEffect(activeCharacter, opponentCharacter) { val playerMaxHP = activeCharacter?.baseHp?.toFloat() ?: 100f @@ -238,6 +243,61 @@ fun BattleScreen( battleSystem.initializeHP(playerMaxHP, opponentMaxHP) } + // Start background music when battle starts + LaunchedEffect(Unit) { + context?.let { ctx -> + try { + // Get external storage directory + val externalDir = Environment.getExternalStorageDirectory() + val musicDir = File(externalDir, "VBHelper/extracted_audio/background_music") + + // Pick a random BGM file (bgm_001.wav to bgm_004.wav) + val bgmNumber = kotlin.random.Random.nextInt(1, 5) // 1 to 4 + val bgmFileName = String.format("bgm_%03d.wav", bgmNumber) + val bgmFile = File(musicDir, bgmFileName) + + if (bgmFile.exists()) { + println("BATTLESCREEN: Starting background music: $bgmFileName") + val player = MediaPlayer().apply { + setDataSource(bgmFile.absolutePath) + prepare() + setOnCompletionListener { + // Stop after one playthrough + println("BATTLESCREEN: Background music completed, stopping") + it.release() + mediaPlayer = null + } + start() + } + mediaPlayer = player + } else { + println("BATTLESCREEN: Background music file not found: ${bgmFile.absolutePath}") + } + } catch (e: Exception) { + println("BATTLESCREEN: Error starting background music: ${e.message}") + e.printStackTrace() + } + } + } + + // Clean up MediaPlayer when battle ends or composable is disposed + DisposableEffect(Unit) { + onDispose { + mediaPlayer?.let { player -> + try { + if (player.isPlaying) { + player.stop() + } + player.release() + println("BATTLESCREEN: Background music stopped and released") + } catch (e: Exception) { + println("BATTLESCREEN: Error stopping background music: ${e.message}") + } + } + mediaPlayer = null + } + } + // Pending damage state for API integration var pendingPlayerDamage by remember { mutableStateOf(0f) } var pendingOpponentDamage by remember { mutableStateOf(0f) } @@ -2307,9 +2367,13 @@ fun BattlesScreen() { } } - // Exit button + // Exit button - stop music before exiting Button( - onClick = { currentView = "main" }, + onClick = { + // Stop background music before exiting + // Note: Music will also be stopped by DisposableEffect in BattleScreen + currentView = "main" + }, modifier = Modifier.align(Alignment.TopCenter), colors = ButtonDefaults.buttonColors(containerColor = Color.Red) ) { From 9365bc0215322f52f446765fbe601d06051ca0ee Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 19 Jan 2026 22:16:09 -0500 Subject: [PATCH 83/89] API calls now use X-Session-Token. --- .../vbhelper/battle/AuthInterceptor.kt | 31 +++ .../nacabaro/vbhelper/battle/AuthService.kt | 3 + .../vbhelper/battle/AuthenticateResponse.kt | 3 +- .../vbhelper/battle/RetrofitHelper.kt | 215 ++++++++++++++---- .../vbhelper/screens/BattlesScreen.kt | 61 ++++- .../vbhelper/source/AuthRepository.kt | 18 +- 6 files changed, 274 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt new file mode 100644 index 0000000..df03762 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthInterceptor.kt @@ -0,0 +1,31 @@ +package com.github.nacabaro.vbhelper.battle + +import okhttp3.Interceptor +import okhttp3.Response + +/** + * OkHttp interceptor that adds Authorization header to API requests. + * Skips adding header for auth endpoints. + */ +class AuthInterceptor(private val token: String) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + // Skip adding auth header for auth endpoints + if (originalRequest.url.encodedPath.startsWith("/api/auth")) { + return chain.proceed(originalRequest) + } + + // Add authentication header for game endpoints + // Use X-Session-Token header (preferred) or Authorization: Bearer + val authenticatedRequest = originalRequest.newBuilder() + .header("X-Session-Token", token) + .build() + + // Debug: Log which header is being used (first few chars of token for security) + val tokenPreview = if (token.length > 8) "${token.take(4)}...${token.takeLast(4)}" else "***" + println("AuthInterceptor: Adding X-Session-Token header (token: $tokenPreview)") + + return chain.proceed(authenticatedRequest) + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt index 11f7b74..72659cc 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthService.kt @@ -7,5 +7,8 @@ import retrofit2.http.POST interface AuthService { @POST("api/auth/validate") fun validate(@Body request: AuthenticateRequest): Call + + @POST("api/auth/login") + fun login(@Body request: AuthenticateRequest): Call } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt index 942791d..cb1f4fd 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AuthenticateResponse.kt @@ -15,6 +15,7 @@ data class UserInfo( data class AuthenticateResponse( val success: Boolean, val message: String? = null, - val userInfo: UserInfo? = null + val userInfo: UserInfo? = null, + val sessionToken: String? = null ) 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 91ba907..99d75ed 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 @@ -7,18 +7,131 @@ import retrofit2.* import retrofit2.converter.gson.GsonConverterFactory import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import com.github.nacabaro.vbhelper.battle.BattleAuthContainer class RetrofitHelper { + + /** + * Creates an OkHttpClient with authentication interceptor for game endpoints. + * Requires a non-null, non-empty token. + */ + private fun createAuthenticatedClient(token: String): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + return OkHttpClient.Builder() + .addInterceptor(AuthInterceptor(token)) + .addInterceptor(loggingInterceptor) + .build() + } + + /** + * Gets the session token from AuthRepository for API calls. + * Falls back to nacatech token if session token is not available (backward compatibility). + */ + private fun getAuthToken(context: Context): String? { + return try { + val authContainer = BattleAuthContainer(context) + runBlocking { + // Prefer session token, fall back to nacatech token for backward compatibility + val sessionToken = authContainer.authRepository.sessionToken.first() + if (!sessionToken.isNullOrEmpty()) { + println("RetrofitHelper: Using sessionToken for API call") + sessionToken + } else { + // Fallback to nacatech token (slower, but works) + val nacatechToken = authContainer.authRepository.authToken.first() + if (!nacatechToken.isNullOrEmpty()) { + println("RetrofitHelper: No sessionToken found, falling back to nacatechToken") + } + nacatechToken + } + } + } catch (e: Exception) { + println("RetrofitHelper: Error getting auth token: ${e.message}") + null + } + } + + /** + * Creates a Retrofit instance with authentication for game endpoints. + */ + private fun createAuthenticatedRetrofit(context: Context): Retrofit? { + val token = getAuthToken(context) + if (token.isNullOrEmpty()) { + println("RetrofitHelper: No auth token available") + return null + } + + val client = createAuthenticatedClient(token) + return Retrofit.Builder() + .baseUrl("http://battle.io-void.com:8080/") + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + /** + * Handles HTTP error responses (401, 403, 429). + * For 401/403, clears authentication state to trigger re-authentication. + */ + private fun handleErrorResponse(context: Context, response: Response<*>, errorMessage: String) { + when (response.code()) { + 401 -> { + println("RetrofitHelper: Authentication failed (401) - token may be expired") + clearAuthAndNotify(context, "Authentication failed. Please log in again.") + } + 403 -> { + println("RetrofitHelper: Access forbidden (403) - token may be expired or invalid") + // 403 could mean expired token, so clear auth state to trigger re-authentication + clearAuthAndNotify(context, "Session expired. Please log in again.") + } + 429 -> { + println("RetrofitHelper: Rate limit exceeded (429)") + Toast.makeText(context, "Too many requests. Please wait a moment.", Toast.LENGTH_SHORT).show() + } + else -> { + println("RetrofitHelper: API error (${response.code()}): $errorMessage") + Toast.makeText(context, "Request failed: ${response.code()}", Toast.LENGTH_SHORT).show() + } + } + } + + /** + * Clears authentication state and shows a message. + * This will trigger BattlesScreen to detect the auth state change and open the login page. + */ + private fun clearAuthAndNotify(context: Context, message: String) { + try { + val authContainer = BattleAuthContainer(context) + CoroutineScope(Dispatchers.IO).launch { + authContainer.authRepository.logout() + println("RetrofitHelper: Cleared authentication state due to expired/invalid token") + } + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } catch (e: Exception) { + println("RetrofitHelper: Error clearing auth state: ${e.message}") + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) { //println("RetrofitHelper: Starting API call for stage: $stage") try { - // Create a Retrofit instance with the base URL and - // a GsonConverterFactory for parsing the response. - val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( - GsonConverterFactory.create()).build() - //println("RetrofitHelper: Retrofit instance created") + // Create an authenticated Retrofit instance + val retrofit = createAuthenticatedRetrofit(context) + if (retrofit == null) { + println("RetrofitHelper: Cannot create authenticated Retrofit - no token available") + Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show() + return + } // Create an ApiService instance from the Retrofit instance. val service: OpponentService = retrofit.create(OpponentService::class.java) @@ -47,13 +160,16 @@ class RetrofitHelper { val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel callback(opponentsList) } else { - println("RetrofitHelper: Response not successful - Error: ${response.errorBody()?.string()}") + val errorBody = response.errorBody()?.string() + println("RetrofitHelper: Response not successful - Error: $errorBody") + handleErrorResponse(context, response, errorBody ?: "Unknown error") } } }) } catch (e: Exception) { println("RetrofitHelper: Exception in getOpponents: ${e.message}") e.printStackTrace() + Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show() } } @@ -62,7 +178,7 @@ class RetrofitHelper { // Create a Retrofit instance with the base URL and // a GsonConverterFactory for parsing the response. - val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory( GsonConverterFactory.create()).build() // Create an ApiService instance from the Retrofit instance. @@ -102,7 +218,7 @@ class RetrofitHelper { // Create a Retrofit instance with the base URL and // a GsonConverterFactory for parsing the response. - val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( + val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory( GsonConverterFactory.create()).build() // Create an ApiService instance from the Retrofit instance. @@ -141,42 +257,58 @@ class RetrofitHelper { fun getPVPWinner(context: Context, apiStage: Int, playerID: Int, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) { - // Create a Retrofit instance with the base URL and - // a GsonConverterFactory for parsing the response. - val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://192.168.0.230:8080/").addConverterFactory( - GsonConverterFactory.create()).build() - - // Create an ApiService instance from the Retrofit instance. - val service: PVPService = retrofit.create(PVPService::class.java) - - // Call the getwinner() method of the ApiService - // to make an API request. - val call: Call = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage) - - // Use the enqueue() method of the Call object to - // make an asynchronous API request. - call.enqueue(object : Callback { - // This is an anonymous inner class that implements the Callback interface. - - override fun onFailure(call: Call, t: Throwable) { - // This method is called when the API request fails. - Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() + try { + // Create an authenticated Retrofit instance + val retrofit = createAuthenticatedRetrofit(context) + if (retrofit == null) { + println("RetrofitHelper: Cannot create authenticated Retrofit - no token available") + Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show() + return } - override fun onResponse(call: Call, response: Response) { - // This method is called when the API response is received successfully. + // Create an ApiService instance from the Retrofit instance. + val service: PVPService = retrofit.create(PVPService::class.java) - if(response.isSuccessful){ - // If the response is successful, parse the - // response body to a DataModel object. - val apiResults: PVPDataModel = response.body() as PVPDataModel + // Call the getwinner() method of the ApiService + // to make an API request. + val call: Call = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage) - // Call the callback function with the DataModel - // object as a parameter. - callback(apiResults) + // Use the enqueue() method of the Call object to + // make an asynchronous API request. + call.enqueue(object : Callback { + // This is an anonymous inner class that implements the Callback interface. + + override fun onFailure(call: Call, t: Throwable) { + // This method is called when the API request fails. + println("RetrofitHelper: PVP API call failed: ${t.message}") + t.printStackTrace() + Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show() } - } - }) + + override fun onResponse(call: Call, response: Response) { + // This method is called when the API response is received successfully. + println("RetrofitHelper: PVP API response received - Code: ${response.code()}") + + if(response.isSuccessful){ + // If the response is successful, parse the + // response body to a DataModel object. + val apiResults: PVPDataModel = response.body() as PVPDataModel + + // Call the callback function with the DataModel + // object as a parameter. + callback(apiResults) + } else { + val errorBody = response.errorBody()?.string() + println("RetrofitHelper: PVP API response not successful - Code: ${response.code()}, Error: $errorBody") + handleErrorResponse(context, response, errorBody ?: "Unknown error") + } + } + }) + } catch (e: Exception) { + println("RetrofitHelper: Exception in getPVPWinner: ${e.message}") + e.printStackTrace() + Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show() + } } fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) { @@ -198,14 +330,15 @@ class RetrofitHelper { .build() val retrofit: Retrofit = Retrofit.Builder() - .baseUrl("http://192.168.0.230:8080/") + .baseUrl("http://battle.io-void.com:8080/") .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() val service: AuthService = retrofit.create(AuthService::class.java) val request = AuthenticateRequest(userToken = token) - val call: Call = service.validate(request) + // Use login endpoint instead of validate to get sessionToken + val call: Call = service.login(request) call.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { 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 aac0482..fae0e01 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 @@ -72,6 +72,7 @@ import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage import com.github.nacabaro.vbhelper.battle.HitEffectOverlay import com.github.nacabaro.vbhelper.battle.BattleAuthContainer import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collect import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items //import kotlin.math.sin @@ -805,7 +806,7 @@ fun MiddleBattleView( ) { // Enemy HP bar (top) LinearProgressIndicator( - progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f), + progress = { battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f) }, modifier = getLandscapeModifier(), color = Color.Red, trackColor = Color.Gray @@ -1017,7 +1018,7 @@ fun MiddleBattleView( ) { // Critical bar LinearProgressIndicator( - progress = battleSystem.critBarProgress / 100f, + progress = { battleSystem.critBarProgress / 100f }, modifier = getLandscapeModifier(), color = Color.Yellow, trackColor = Color.Gray @@ -1035,7 +1036,7 @@ fun MiddleBattleView( ) { // Player HP bar LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + progress = { battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) }, modifier = getLandscapeModifier(), color = Color.Green, trackColor = Color.Gray @@ -1215,7 +1216,7 @@ fun PlayerBattleView( ) { // Health bar LinearProgressIndicator( - progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + progress = { battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) }, modifier = getLandscapeModifier(), color = Color.Green, trackColor = Color.Gray @@ -1401,7 +1402,7 @@ fun EnemyBattleView( ) { // Enemy HP bar LinearProgressIndicator( - progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), + progress = { battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f) }, modifier = getLandscapeModifier(), color = Color.Red, trackColor = Color.Gray @@ -1765,11 +1766,20 @@ fun BattlesScreen() { // Exchange token with battle server RetrofitHelper().authenticate(context, token) { response -> if (response.success) { - // Token already marked as processed before API call, just extract userId - // Extract userId from response + // Extract userId and sessionToken from response val extractedUserId = response.userInfo?.userId?.toLongOrNull() + val sessionToken = response.sessionToken + + println("BATTLESCREEN: Authentication successful, userId: $extractedUserId, sessionToken: ${if (sessionToken != null) "present" else "missing"}") + + // Store both nacatech token (for re-auth) and sessionToken (for API calls) kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { - battleAuthContainer.authRepository.setAuthenticated(true, token, extractedUserId) + battleAuthContainer.authRepository.setAuthenticated( + isAuthenticated = true, + nacatechToken = token, + sessionToken = sessionToken, + userId = extractedUserId + ) } // Update UI state on main thread kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { @@ -1877,16 +1887,23 @@ fun BattlesScreen() { kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch { if (response.success) { val extractedUserId = response.userInfo?.userId?.toLongOrNull() - // Update stored userId + val sessionToken = response.sessionToken + + // Update stored userId and sessionToken if (extractedUserId != null) { kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { - authRepository.setAuthenticated(true, storedToken, extractedUserId) + authRepository.setAuthenticated( + isAuthenticated = true, + nacatechToken = storedToken, + sessionToken = sessionToken, + userId = extractedUserId + ) } } isAuthenticated = true isCheckingAuth = false userId = extractedUserId - println("BATTLESCREEN: Got userId from validation: $extractedUserId") + println("BATTLESCREEN: Got userId from validation: $extractedUserId, sessionToken: ${if (sessionToken != null) "present" else "missing"}") } else { println("BATTLESCREEN: Token validation failed: ${response.message}") // Check if it's a critical error that requires re-authentication @@ -1999,6 +2016,28 @@ fun BattlesScreen() { } } + // Watch auth repository state changes to detect when token is cleared (e.g., expired token) + LaunchedEffect(Unit) { + battleAuthContainer.authRepository.isAuthenticated.collect { authState -> + if (!authState && isAuthenticated) { + // Auth state was cleared (e.g., by RetrofitHelper due to expired token) + println("BATTLESCREEN: Auth state cleared, triggering re-authentication") + isAuthenticated = false + isCheckingAuth = false + // Open auth URL to get a fresh token + val authUrl = "http://auth.nacatech.es/begin?app=443654920&redirect_uri=vbhelper://auth?token=" + val authIntent = Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)) + try { + context.startActivity(authIntent) + println("BATTLESCREEN: Opened auth URL after token expiration: $authUrl") + } catch (e: Exception) { + println("BATTLESCREEN: Failed to open auth URL: ${e.message}") + e.printStackTrace() + } + } + } + } + // Also check intent when authentication state changes // Only process if it's a fresh ACTION_VIEW intent (deep link) LaunchedEffect(isAuthenticated) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt index c630ca9..53bc737 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/AuthRepository.kt @@ -14,7 +14,8 @@ class AuthRepository( ) { private companion object { val IS_AUTHENTICATED = booleanPreferencesKey("is_authenticated") - val AUTH_TOKEN = stringPreferencesKey("auth_token") + val AUTH_TOKEN = stringPreferencesKey("auth_token") // Nacatech token (for re-authentication) + val SESSION_TOKEN = stringPreferencesKey("session_token") // Session token (for API calls) val USER_ID = longPreferencesKey("user_id") } @@ -28,16 +29,24 @@ class AuthRepository( preferences[AUTH_TOKEN] } + val sessionToken: Flow = dataStore.data + .map { preferences -> + preferences[SESSION_TOKEN] + } + val userId: Flow = dataStore.data .map { preferences -> preferences[USER_ID] } - suspend fun setAuthenticated(isAuthenticated: Boolean, token: String? = null, userId: Long? = null) { + suspend fun setAuthenticated(isAuthenticated: Boolean, nacatechToken: String? = null, sessionToken: String? = null, userId: Long? = null) { dataStore.edit { preferences -> preferences[IS_AUTHENTICATED] = isAuthenticated - if (token != null) { - preferences[AUTH_TOKEN] = token + if (nacatechToken != null) { + preferences[AUTH_TOKEN] = nacatechToken + } + if (sessionToken != null) { + preferences[SESSION_TOKEN] = sessionToken } if (userId != null) { preferences[USER_ID] = userId @@ -49,6 +58,7 @@ class AuthRepository( dataStore.edit { preferences -> preferences[IS_AUTHENTICATED] = false preferences.remove(AUTH_TOKEN) + preferences.remove(SESSION_TOKEN) preferences.remove(USER_ID) } } From a9c354ad8a94bbef6ca2d9bb0e8f08d71f1087a3 Mon Sep 17 00:00:00 2001 From: lightheel Date: Mon, 19 Jan 2026 22:21:05 -0500 Subject: [PATCH 84/89] Updated to use Long instead of Int on playerID. --- .../com/github/nacabaro/vbhelper/battle/PVPService.kt | 2 +- .../com/github/nacabaro/vbhelper/battle/RetrofitHelper.kt | 2 +- .../com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) 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 8bc6ba2..fa7c19f 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,5 @@ 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: Int, @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): 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 99d75ed..f4771a4 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 @@ -255,7 +255,7 @@ class RetrofitHelper { } */ - fun getPVPWinner(context: Context, apiStage: Int, playerID: Int, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) { + fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) { try { // Create an authenticated Retrofit instance 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 fae0e01..f0db0cd 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 @@ -1097,7 +1097,7 @@ fun MiddleBattleView( RetrofitHelper().getPVPWinner( ctx, 1, - playerUserId?.toInt() ?: 2, + playerUserId ?: 2L, activeCharacter?.name ?: "Player", playerStage, opponentStage, @@ -2268,7 +2268,7 @@ fun BattlesScreen() { else -> 0 } - RetrofitHelper().getPVPWinner(context, 0, userId?.toInt() ?: 2, cardId, apiStage, 0, opponent.charaId, apiStage) { apiResult -> + 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, @@ -2349,7 +2349,7 @@ fun BattlesScreen() { RetrofitHelper().getPVPWinner( context, 1, - userId?.toInt() ?: 2, + userId ?: 2L, activeCharacter?.name ?: "Player", playerStage, opponentStage, @@ -2363,7 +2363,7 @@ fun BattlesScreen() { RetrofitHelper().getPVPWinner( context, 2, - userId?.toInt() ?: 2, + userId ?: 2L, activeCharacter?.name ?: "Player", playerStage, opponentStage, From 2201b7d0feb5380a939b65e826491c56cd948c23 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 21 Jan 2026 20:23:51 -0500 Subject: [PATCH 85/89] 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 From 65a7ccb2219de232138d42a66a4d3c2a52fa32cb Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 21 Jan 2026 21:27:11 -0500 Subject: [PATCH 86/89] Fixed resume battle max HP display bug. --- .../nacabaro/vbhelper/battle/PVPDataModel.kt | 4 +++- .../nacabaro/vbhelper/screens/BattlesScreen.kt | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) 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 c30b47e..9e0c237 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 @@ -10,5 +10,7 @@ data class PVPDataModel ( val playerAttackDamage: Int, val opponentAttackDamage: Int, val winner: String, - val opponentCharaId: String? = null // TODO: Server will add this - opponent's charaId from the match + val opponentCharaId: String? = null, // Server provides opponent's charaId from the match + val playerMaxHP: Int? = null, // Server should provide max HP for resumed matches + val opponentMaxHP: Int? = null // Server should provide max HP for resumed matches ):java.io.Serializable \ No newline at end of file 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 3d2cda4..fb480df 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 @@ -2502,9 +2502,14 @@ fun BattlesScreen() { println("BATTLESCREEN: Clicked opponent: ${clickedOpponent.name} (${clickedOpponent.charaId}), baseHp: ${clickedOpponent.baseHp}") // Update player character HP from API response + // Use playerMaxHP from API if available, otherwise use current HP as fallback + // NOTE: Server should provide playerMaxHP for resumed matches since DB stats use different scaling + val playerMaxHp = apiResult.playerMaxHP ?: apiResult.playerHP + println("BATTLESCREEN: Resuming match - playerMaxHP from API: ${apiResult.playerMaxHP}, using: $playerMaxHp") + activeCharacter = activeCharacter?.copy( - baseHp = apiResult.playerHP, - currentHp = apiResult.playerHP + baseHp = playerMaxHp, // Use max HP from API (or current HP as fallback) + currentHp = apiResult.playerHP // Current HP from API ) // Find the actual opponent from the match @@ -2583,9 +2588,13 @@ fun BattlesScreen() { 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 + // Use opponentMaxHP from API if available, otherwise use opponent's baseHp from opponentsList + // NOTE: Server should provide opponentMaxHP for resumed matches since DB stats use different scaling + val opponentMaxHp = apiResult.opponentMaxHP ?: actualOpponent.baseHp + println("BATTLESCREEN: Resuming match - opponentMaxHP from API: ${apiResult.opponentMaxHP}, using: $opponentMaxHp") + selectedOpponent = actualOpponent.copy( - baseHp = actualOpponent.baseHp, // Keep original baseHp + baseHp = opponentMaxHp, // Use max HP from API (or opponent's baseHp as fallback) currentHp = apiResult.opponentHP // Use current HP from match ) From 566e2ec9773a9007e00497cea531e3883b43eb22 Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 21 Jan 2026 22:53:42 -0500 Subject: [PATCH 87/89] Battles now update wins/losses in room DB. --- .../vbhelper/screens/BattlesScreen.kt | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) 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 fb480df..66ecbff 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 @@ -2409,10 +2409,43 @@ fun BattlesScreen() { selectedOpponent?.name ?: "Opponent", opponentStage ) { apiResult -> - winnerName = apiResult.winner ?: "Unknown" + // Winner might be empty in first call, but we can check HP values + // If opponentHP is negative, player won. If playerHP is negative or 0, player lost. + val playerWonFromHP = apiResult.opponentHP <= 0 && apiResult.playerHP > 0 + + // Also check winner field if it's not empty + val playerWonFromWinner = activeCardId?.let { cardId -> + val winner = apiResult.winner ?: "" + if (winner.isNotEmpty()) { + if (winner.contains("|")) { + // Pipe-separated format: first part is the winner + val winnerParts = winner.split("|") + val winnerCharaId = winnerParts.getOrNull(0) ?: "" + winnerCharaId.contains(cardId, ignoreCase = true) + } else { + // Simple format: check if winner contains player's charaId or matches player name + winner.contains(cardId, ignoreCase = true) || + winner.equals(activeCharacter?.name, ignoreCase = true) + } + } else { + false + } + } ?: false + + // Use HP-based determination if winner field is empty, otherwise use winner field + val playerWon = if (apiResult.winner.isNullOrEmpty()) { + playerWonFromHP + } else { + playerWonFromWinner + } + + println("BATTLESCREEN: Battle result (first call) - winner: '${apiResult.winner}', playerHP: ${apiResult.playerHP}, opponentHP: ${apiResult.opponentHP}, playerWonFromHP: $playerWonFromHP, playerWonFromWinner: $playerWonFromWinner, final playerWon: $playerWon") + + // Store winner name for display (will be updated in cleanup call if available) + winnerName = apiResult.winner ?: if (playerWon) "You" else "Opponent" isWinnerLoaded = true - // Then send the cleanup call + // Then send the cleanup call - this will have the actual winner name RetrofitHelper().getPVPWinner( context, 2, @@ -2423,6 +2456,85 @@ fun BattlesScreen() { selectedOpponent?.name ?: "Opponent", opponentStage ) { cleanupResult -> + // Update winner name from cleanup call if available + if (cleanupResult.winner.isNotEmpty()) { + winnerName = cleanupResult.winner + } + + // Determine final winner from cleanup call (most reliable) + // Primary method: Check HP values (opponentHP <= 0 means opponent lost = player won) + // Secondary method: Check winner name (if winner doesn't match opponent, player won) + val opponentName = selectedOpponent?.name ?: "" + val winner = cleanupResult.winner ?: "" + + // Primary: HP-based determination (most reliable) + // If opponentHP <= 0, opponent is dead = player won + // If playerHP <= 0, player is dead = player lost + val playerWonFromHP = cleanupResult.opponentHP <= 0 && cleanupResult.playerHP > 0 + + // Secondary: Winner name-based determination (only if HP check is inconclusive) + val playerWonFromName = if (winner.isNotEmpty() && opponentName.isNotEmpty()) { + // If winner matches opponent name, player lost. Otherwise, player won. + !winner.equals(opponentName, ignoreCase = true) + } else if (winner.isNotEmpty()) { + // Check if winner matches player's charaId + activeCardId?.let { cardId -> + if (winner.contains("|")) { + val winnerParts = winner.split("|") + val winnerCharaId = winnerParts.getOrNull(0) ?: "" + winnerCharaId.contains(cardId, ignoreCase = true) + } else { + winner.contains(cardId, ignoreCase = true) + } + } ?: false + } else { + false + } + + // Use HP as primary (most reliable), name as fallback only if HP values are both positive + val finalPlayerWon = if (cleanupResult.opponentHP <= 0 || cleanupResult.playerHP <= 0) { + // HP clearly indicates winner + playerWonFromHP + } else { + // Both have HP (shouldn't happen in cleanup, but use name as fallback) + playerWonFromName + } + + println("BATTLESCREEN: Battle result (cleanup call) - winner: '${cleanupResult.winner}', playerHP: ${cleanupResult.playerHP}, opponentHP: ${cleanupResult.opponentHP}, finalPlayerWon: $finalPlayerWon") + + // Update battle stats in database using the most reliable determination + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + try { + val application = context.applicationContext as VBHelper + val database = application.container.db + val activeChar = database.userCharacterDao().getActiveCharacter() + + if (activeChar != null) { + // Get the full UserCharacter entity to update + val userCharacter = database.userCharacterDao().getCharacter(activeChar.id) + + // Update battle stats + if (finalPlayerWon) { + userCharacter.currentPhaseBattlesWon += 1 + userCharacter.totalBattlesWon += 1 + println("BATTLESCREEN: Player won - updated wins: currentPhase=${userCharacter.currentPhaseBattlesWon}, total=${userCharacter.totalBattlesWon}") + } else { + userCharacter.currentPhaseBattlesLost += 1 + userCharacter.totalBattlesLost += 1 + println("BATTLESCREEN: Player lost - updated losses: currentPhase=${userCharacter.currentPhaseBattlesLost}, total=${userCharacter.totalBattlesLost}") + } + + // Save updated character to database + database.userCharacterDao().updateCharacter(userCharacter) + println("BATTLESCREEN: Updated battle stats in database") + } else { + println("BATTLESCREEN: WARNING: Could not find active character to update battle stats") + } + } catch (e: Exception) { + println("BATTLESCREEN: Error updating battle stats: ${e.message}") + e.printStackTrace() + } + } } } } From 977896d29620ce3a4443cc8b98ce73ecf788ccf8 Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 6 Mar 2026 12:20:28 -0500 Subject: [PATCH 88/89] Merge bugfixes. --- app/build.gradle.kts | 15 +++++++++++++++ .../nacabaro/vbhelper/screens/BattlesScreen.kt | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ff1234..99c898f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,4 +103,19 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) + + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + implementation("androidx.navigation:navigation-compose:2.7.0") + implementation("com.google.android.material:material:1.2.0") + implementation(libs.protobuf.javalite) + implementation("androidx.compose.material:material") + implementation("androidx.datastore:datastore-preferences:1.1.7") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") + + // HTTP request logging + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") } \ No newline at end of file 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 66ecbff..b2581c0 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 @@ -2111,7 +2111,7 @@ fun BattlesScreen() { } */ - val activeChar = database.userCharacterDao().getActiveCharacter() + val activeChar = database.userCharacterDao().getActiveCharacter().first() //println("BATTLESCREEN: getActiveCharacter() returned: $activeChar") if (activeChar != null) { @@ -2507,7 +2507,7 @@ fun BattlesScreen() { try { val application = context.applicationContext as VBHelper val database = application.container.db - val activeChar = database.userCharacterDao().getActiveCharacter() + val activeChar = database.userCharacterDao().getActiveCharacter().first() if (activeChar != null) { // Get the full UserCharacter entity to update From 3f29d725eaac3d4e95e9fd2759e12f3d9aa6ff9d Mon Sep 17 00:00:00 2001 From: lightheel Date: Fri, 6 Mar 2026 15:16:43 -0500 Subject: [PATCH 89/89] Updated opponent list to show display name. --- .../com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt | 3 ++- .../java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt index ddf3102..d2b9330 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/APIBattleCharacter.kt @@ -9,5 +9,6 @@ data class APIBattleCharacter( val baseHp: Int, val currentHp: Int, val baseBp: Float, - val baseAp: Float + val baseAp: Float, + val displayName: String? = null ) \ No newline at end of file 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 b2581c0..dad71ac 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 @@ -2335,7 +2335,7 @@ fun BattlesScreen() { }, modifier = Modifier.fillMaxWidth() ) { - Text("Battle ${opponent.name}") + Text("Battle ${opponent.displayName?.takeIf { it.isNotBlank() } ?: opponent.name}") } } }