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
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e1499af..42ba584 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..d8da12c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,9 @@
+
+
+
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) }
+
+ // 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 {
+ animationStateMachine.playIdleAnimation()
+ }
+ }
+
+ // Change animation when animationType changes
+ LaunchedEffect(animationType) {
+ coroutineScope.launch {
+ if (animationType == DigimonAnimationType.IDLE) {
+ animationStateMachine.playIdleAnimation()
+ } else {
+ animationStateMachine.playAnimation(animationType)
+ }
+ }
+ }
+
+ // 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")
+ }
+ }
+
+ 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/ArenaBattleSystem.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt
new file mode 100644
index 0000000..61073fe
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt
@@ -0,0 +1,420 @@
+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
+import androidx.compose.runtime.State
+
+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
+
+ // 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
+
+ // 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
+
+ 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
+
+ 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
+ _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 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)
+ }
+ 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
+ _isDodging = false
+ _dodgeProgress = 0f
+ _dodgeDirection = 1f
+ _isHit = false
+ _hitProgress = 0f
+ _isPlayerDodging = false
+ _isOpponentDodging = false
+ _playerDodgeProgress = 0f
+ _playerDodgeDirection = 1f
+ _opponentDodgeProgress = 0f
+ _opponentDodgeDirection = 1f
+ _isPlayerHit = false
+ _isOpponentHit = false
+ _isPlayerHitDelayed = false
+ _isOpponentHitDelayed = false
+ _isPlayerShakeDelayed = false
+ _isOpponentShakeDelayed = false
+ _shouldCounterAttack = false
+ _counterAttackIsHit = false
+ _opponentAttackIsHit = false
+ 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")
+ }
+
+ // 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
+ _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) {
+ _playerDodgeProgress = progress
+ }
+
+ fun setPlayerDodgeDirection(direction: Float) {
+ _playerDodgeDirection = direction
+ }
+
+ // Opponent-specific dodge methods
+ fun startOpponentDodge() {
+ _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) {
+ _opponentDodgeProgress = progress
+ }
+
+ fun setOpponentDodgeDirection(direction: Float) {
+ _opponentDodgeDirection = direction
+ }
+
+ // Player-specific hit methods
+ 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
+ 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/battle/AttackSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt
new file mode 100644
index 0000000..4f7d1ec
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/AttackSpriteManager.kt
@@ -0,0 +1,165 @@
+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
+
+data class CharacterData(
+ val name: String,
+ val charaId: String,
+ val smalefilename: String,
+ 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()
+
+ // 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")
+ 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") {
+ println("AttackSpriteManager: Skipping attack file (filename is '0')")
+ return null
+ }
+
+ // 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()}")
+
+ 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")
+ 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 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()}")
+
+ if (!characterDataFile.exists()) {
+ 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()
+ println("AttackSpriteManager: JSON content length = ${jsonContent.length}")
+
+ // 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,
+ smalefilename = "atk_s_02", // Default small attack
+ laugeFileName = "atk_l_04" // Default large attack
+ )
+
+ 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
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..f7fd5c1
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/BattleSpriteManager.kt
@@ -0,0 +1,181 @@
+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
+
+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()
+
+ // 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}"
+
+ // Check cache first
+ if (spriteCache.containsKey(cacheKey)) {
+ return spriteCache[cacheKey]
+ }
+
+ // Debug: Check if base directory exists
+ val spriteBaseDir = getSpriteBaseDir()
+ 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 PNG texture file directly using the atlas name
+ val textureFile = File(spriteBaseDir, "extracted_textures/${atlasName}.png")
+
+ 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/${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)
+
+ // 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(),
+ correctedY,
+ spriteData.texture_rect.width.toInt(),
+ spriteData.texture_rect.height.toInt()
+ )
+
+ // 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
+ }
+
+ println("Extracted sprite dimensions: ${finalBitmap.width}x${finalBitmap.height}")
+
+ // Cache the result
+ spriteCache[cacheKey] = finalBitmap
+
+ return finalBitmap
+
+ } 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(getSpriteBaseDir(), "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 texturesDir = File(getSpriteBaseDir(), "extracted_textures")
+ if (!texturesDir.exists()) {
+ return emptyList()
+ }
+
+ val textureFiles = texturesDir.listFiles { file ->
+ file.name.endsWith(".png")
+ } ?: emptyArray()
+
+ return textureFiles.map { file ->
+ // Extract atlas name from filename (e.g., "dim000_mon01.png" -> "dim000_mon01")
+ file.name.substringBefore(".png")
+ }.sorted()
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return emptyList()
+ }
+ }
+}
\ No newline at end of file
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..d23ea11
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/DigimonAnimationState.kt
@@ -0,0 +1,169 @@
+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
+import android.content.Context
+import java.io.File
+
+enum class DigimonAnimationType {
+ IDLE,
+ IDLE2,
+ WALK,
+ WALK2,
+ RUN,
+ RUN2,
+ WORKOUT,
+ WORKOUT2,
+ HAPPY,
+ SLEEP,
+ ATTACK,
+ FLEE
+}
+
+data class AnimationState(
+ val type: DigimonAnimationType,
+ val frameNumber: Int, // 1-12 for individual PNG files
+ val duration: Long = 100L, // Duration in milliseconds
+ val loop: Boolean = true
+)
+
+class DigimonAnimationStateMachine(
+ private val characterId: String,
+ 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
+
+ var currentFrameNumber by mutableStateOf(1)
+ private set
+
+ var isPlaying by mutableStateOf(false)
+ private set
+
+ // 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
+ )
+
+ // 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(
+ DigimonAnimationType.IDLE to 750L,
+ DigimonAnimationType.IDLE2 to 750L,
+ 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 1500L,
+ 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) {
+ return // Already playing this animation
+ }
+
+ currentAnimation = animationType
+ isPlaying = true
+
+ 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
+ delay(duration)
+ playAnimation(DigimonAnimationType.IDLE)
+ } else {
+ // For looping animations, cycle through frames
+ var frameIndex = 0
+ while (isPlaying && currentAnimation == animationType) {
+ val frameNumber = frameNumbers[frameIndex % frameNumbers.size]
+ currentFrameNumber = frameNumber
+ delay(duration)
+ frameIndex++
+ }
+ }
+ }
+
+ // 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
+
+ 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()
+
+ println("Playing idle animation with frames: $combinedFrames, starting at offset: $initialFrameOffset, timing offset: $timingOffset")
+
+ val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L
+
+ // 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
+ delay(duration)
+ frameIndex++
+ }
+ }
+
+ fun stopAnimation() {
+ isPlaying = false
+ }
+
+ fun getCurrentFrame(): Int {
+ return currentFrameNumber
+ }
+
+ 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/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt
new file mode 100644
index 0000000..1f3210e
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt
@@ -0,0 +1,121 @@
+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.platform.LocalConfiguration
+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 configuration = LocalConfiguration.current
+ val isLandscapeMode = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
+ 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")
+
+ // 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"
+ 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
+ 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
+ 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)
+ ),
+ 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..6536040
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt
@@ -0,0 +1,174 @@
+package com.github.nacabaro.vbhelper.battle
+
+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()
+
+ // 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)
+ * @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 hitSpritesDir = getHitSpritesDir()
+ 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(getHitSpritesDir(), "$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 all available hit sprites
+ * @return List of hit sprite names (without .png extension)
+ */
+ fun getAvailableHitSprites(): List {
+ 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()
+ }
+
+ /**
+ * Get available damage effect spritesheet names
+ * @return List of available damage effect spritesheet names
+ */
+ fun getAvailableDamageEffectSpritesheets(): List {
+ try {
+ if (!getHitSpritesDir().exists()) {
+ return emptyList()
+ }
+
+ val dmgFiles = getHitSpritesDir().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/battle/IndividualSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt
new file mode 100644
index 0000000..f6b9179
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/IndividualSpriteManager.kt
@@ -0,0 +1,134 @@
+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()
+
+ // 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
+ * @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
+ val spriteBaseDir = getSpriteBaseDir()
+ 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 {
+ 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 character IDs
+ * @return List of character IDs that have sprite directories
+ */
+ fun getAvailableCharacters(): List {
+ 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()
+ }
+
+ /**
+ * 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(getSpriteBaseDir(), 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/OpponentService.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentService.kt
new file mode 100644
index 0000000..43dc60b
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/OpponentService.kt
@@ -0,0 +1,13 @@
+package com.github.nacabaro.vbhelper.battle
+
+import retrofit2.Call
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface OpponentService {
+ @GET("api/opponents")
+ // This method returns a Call object with a generic
+ // type of DataModel, which represents
+ // the data model for the response.
+ fun getopponents(@Query("stage") stage: String): Call
+}
\ 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/battle/SpriteFileManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt
new file mode 100644
index 0000000..2f663cb
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteFileManager.kt
@@ -0,0 +1,242 @@
+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) {
+
+ // 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 to external storage...")
+
+ // 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(", ")}")
+ }
+
+ // 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(", ")}")
+ }
+
+ // 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}")
+ }
+ }
+
+ // Create the base directory for battle_sprites in external storage
+ val battleSpritesDir = getSpriteBaseDir()
+ if (!battleSpritesDir.exists()) {
+ battleSpritesDir.mkdirs()
+ println("Created battle_sprites directory in external storage: ${battleSpritesDir.absolutePath}")
+ } else {
+ println("battle_sprites directory already exists in external storage: ${battleSpritesDir.absolutePath}")
+ }
+
+ // 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)
+ println("Copying $sourcePath to ${targetDir.absolutePath}")
+ copyAssetDirectory(sourcePath, targetDir)
+ }
+
+ println("Sprite files copied successfully to external storage: ${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 to external storage: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ private fun copyAssetDirectory(assetPath: String, targetDir: File) {
+ try {
+ 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)
+
+ // Create subdirectories if needed
+ if (targetFile.parentFile != null && !targetFile.parentFile!!.exists()) {
+ targetFile.parentFile!!.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)
+ }
+ } 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()
+ }
+ }
+
+ 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 battleSpritesDir = getSpriteBaseDir()
+ 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
+
+ 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 battleSpritesDir = getSpriteBaseDir()
+
+ if (battleSpritesDir.exists()) {
+ deleteDirectory(battleSpritesDir)
+ 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
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..f7d4430
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/SpriteImage.kt
@@ -0,0 +1,40 @@
+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
+
+@Composable
+fun SpriteImage(
+ characterId: String,
+ frameNumber: Int,
+ modifier: Modifier = Modifier,
+ contentScale: ContentScale = ContentScale.Fit
+) {
+ val context = LocalContext.current
+ val spriteManager = remember { IndividualSpriteManager(context) }
+
+ var bitmap by remember { mutableStateOf(null) }
+
+ LaunchedEffect(characterId, frameNumber) {
+ println("Loading sprite frame: $frameNumber for character: $characterId")
+ 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")
+ }
+ }
+
+ bitmap?.let { bmp ->
+ Image(
+ bitmap = bmp.asImageBitmap(),
+ contentDescription = "Sprite: $characterId frame $frameNumber",
+ modifier = modifier,
+ contentScale = contentScale
+ )
+ }
+}
\ 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
new file mode 100644
index 0000000..51f4ed3
--- /dev/null
+++ b/app/src/main/java/com/github/nacabaro/vbhelper/components/AttackSpriteImage.kt
@@ -0,0 +1,46 @@
+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) {
+ 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
+ }
+ }
+
+ bitmap?.let { bmp ->
+ Image(
+ bitmap = bmp.asImageBitmap(),
+ contentDescription = "Attack Sprite",
+ modifier = modifier,
+ contentScale = contentScale
+ )
+ }
+}
\ 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..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
@@ -4,20 +4,1921 @@ 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.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.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.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
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.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
+import com.github.nacabaro.vbhelper.battle.HitEffectOverlay
+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
+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(
+ 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,
+ playerName: String,
+ opponentName: String,
+ activeCharacter: APIBattleCharacter?,
+ opponentCharacter: APIBattleCharacter?,
+ onAttackClick: () -> Unit,
+ context: android.content.Context? = null,
+ selectedBackgroundSet: Int = 0
+) {
+ val battleSystem = remember { ArenaBattleSystem() }
+ val coroutineScope = rememberCoroutineScope()
+
+ // 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) }
+
+ // 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) }
+
+ // Hit effect animation state
+ 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
+ battleSystem.endPlayerHitDelayed()
+ battleSystem.endOpponentHitDelayed()
+ battleSystem.endPlayerShakeDelayed()
+ battleSystem.endOpponentShakeDelayed()
+ println("DEBUG: Reset hit effect states, attack sprite visibility, and delayed hit states")
+ }
+ }
+
+ // Critical bar timer
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(30)
+ if (battleSystem.attackPhase == 0) { // Only update when not attacking
+ battleSystem.updateCritBarProgress((battleSystem.critBarProgress + 5) % 101)
+ }
+ }
+ }
+
+ // Animation for attack phases
+ LaunchedEffect(battleSystem.attackPhase) {
+ 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) {
+ progress += 0.016f // 60 FPS
+ battleSystem.setAttackProgress(progress)
+
+ // 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 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
+ }
+ // Delay showing damage number to match hit effect timing
+ if (pendingOpponentDamage > 0) {
+ 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
+ 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(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) {
+ progress += 0.016f // 60 FPS
+ battleSystem.setAttackProgress(progress)
+
+ // 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 {
+ delay(400) // Match the hit effect delay
+ hidePlayerAttackSprite = true
+ }
+ // Delay showing damage number to match hit effect timing
+ if (pendingPlayerDamage > 0) {
+ 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
+ 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()
+
+ // Check if battle is over
+ if (battleSystem.checkBattleOver()) {
+ battleSystem.endBattle()
+ onAttackClick()
+ }
+ }
+ }
+ }
+
+ // 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.setPlayerDodgeProgress(dodgeProgress)
+ battleSystem.setPlayerDodgeDirection(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.setPlayerDodgeProgress(dodgeProgress)
+ battleSystem.setPlayerDodgeDirection(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.setOpponentDodgeProgress(dodgeProgress)
+ battleSystem.setOpponentDodgeDirection(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.setOpponentDodgeProgress(dodgeProgress)
+ battleSystem.setOpponentDodgeDirection(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")
+ }
+ }
+
+ // 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) {
+ 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")
+ }
+ }
+
+ // 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) {
+ println("DEBUG: LaunchedEffect triggered for pendingPlayerDamage = $pendingPlayerDamage")
+ playerDamageValue = pendingPlayerDamage.toInt()
+ // Don't show immediately - wait for attack animation to reach the Digimon
+ }
+ }
+
+ 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
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ when (battleSystem.currentView) {
+ 0 -> {
+ // 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,
+ hidePlayerAttackSprite = hidePlayerAttackSprite,
+ hideEnemyAttackSprite = hideEnemyAttackSprite,
+ selectedBackgroundSet = selectedBackgroundSet
+ )
+ }
+ 1 -> {
+ // Player screen - enemy attack
+ PlayerBattleView(
+ battleSystem = battleSystem,
+ stage = stage,
+ playerName = playerName,
+ attackAnimationProgress = battleSystem.attackProgress,
+ onAttackClick = {
+ battleSystem.startPlayerAttack()
+ },
+ activeCharacter = activeCharacter,
+ context = context,
+ opponent = opponentCharacter,
+ onSetPendingDamage = { playerDamage, opponentDamage ->
+ pendingPlayerDamage = playerDamage
+ pendingOpponentDamage = opponentDamage
+ },
+ coroutineScope = coroutineScope,
+ hidePlayerAttackSprite = hidePlayerAttackSprite,
+ selectedBackgroundSet = selectedBackgroundSet
+ )
+ }
+ 2 -> {
+ // Enemy screen - player attack
+ EnemyBattleView(
+ battleSystem = battleSystem,
+ stage = stage,
+ opponentName = opponentName,
+ attackAnimationProgress = battleSystem.attackProgress,
+ activeCharacter = opponentCharacter,
+ playerCharacter = activeCharacter,
+ hideEnemyAttackSprite = hideEnemyAttackSprite,
+ selectedBackgroundSet = selectedBackgroundSet
+ )
+ }
+ }
+
+ // 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 = (-50).dp)
+ //.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
+ 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,
+ modifier = Modifier
+ .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
+ hideEnemyAttackSprite = false // Show attack sprite again
+ println("DEBUG: Enemy hit effect animation completed")
+ }
+ )
+
+
+ }
+ }
+ }
+}
+
+@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,
+ hidePlayerAttackSprite: Boolean,
+ hideEnemyAttackSprite: Boolean,
+ selectedBackgroundSet: Int = 0
+) {
+ // 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()
+ ) {
+ // Animated background - positioned underneath all other sprites
+ MultiLayerAnimatedBattleBackground(
+ modifier = Modifier.fillMaxSize(),
+ backgroundSetIndex = selectedBackgroundSet
+ )
+
+ // Top section: HP bars and HP numbers
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ // Enemy HP bar and text with background box
+ Box(
+ modifier = getLandscapeBoxModifier(),
+ contentAlignment = getLandscapeAlignment()
+ ) {
+ Column(
+ horizontalAlignment = getLandscapeHorizontalAlignment()
+ ) {
+ // Enemy HP bar (top)
+ LinearProgressIndicator(
+ progress = battleSystem.opponentHP / (opponentCharacter?.baseHp?.toFloat() ?: 100f),
+ modifier = getLandscapeModifier(),
+ 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 = getLandscapeFontSize(),
+ 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))
+
+ // 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.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1
+ battleSystem.isOpponentDodging -> DigimonAnimationType.WALK
+ battleSystem.isOpponentHitDelayed -> DigimonAnimationType.SLEEP
+ 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.isOpponentShakeDelayed) {
+ 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 + 40.dp
+ ),
+ contentScale = ContentScale.Fit,
+ reloadMappings = false,
+ animationOffset = 375L // Offset enemy animation by half the idle duration
+ )
+
+ // Enemy attack sprite (Phase 1 only)
+ 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
+
+ AttackSpriteImage(
+ characterId = opponentCharacter?.charaId ?: "dim011_mon01",
+ isLarge = true,
+ modifier = Modifier
+ .size(60.dp)
+ .offset(
+ x = xOffset,
+ y = yOffset
+ ),
+ contentScale = ContentScale.Fit
+ )
+ }
+ }
+
+ // 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(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ // Determine animation type for player
+ val playerAnimationType = when {
+ battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Both attacking in Phase 1
+ battleSystem.isPlayerDodging -> DigimonAnimationType.WALK
+ battleSystem.isPlayerHitDelayed -> DigimonAnimationType.SLEEP
+ 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.isPlayerShakeDelayed) {
+ 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 - 40.dp
+ ),
+ contentScale = ContentScale.Fit,
+ reloadMappings = false,
+ animationOffset = 0L // Player animation starts immediately
+ )
+
+ // Player attack sprite (Phase 1 only)
+ 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
+
+ AttackSpriteImage(
+ characterId = activeCharacter?.charaId ?: "dim011_mon01",
+ isLarge = true,
+ modifier = Modifier
+ .size(60.dp)
+ .offset(
+ x = xOffset,
+ y = yOffset
+ )
+ .scale(-1f, 1f), // Flip attack sprite
+ contentScale = ContentScale.Fit
+ )
+ }
+ }
+ }
+ }
+
+ // Bottom section: Player HP bar, 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(6.dp))
+
+ // Player HP bar and text with background box
+ Box(
+ modifier = getLandscapeBoxModifier(),
+ contentAlignment = getLandscapeAlignment()
+ ) {
+ Column(
+ horizontalAlignment = getLandscapeHorizontalAlignment()
+ ) {
+ // Player HP bar
+ LinearProgressIndicator(
+ progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f),
+ modifier = getLandscapeModifier(),
+ 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 = getLandscapeFontSize(),
+ 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))
+
+ // 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)
+
+ // 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(0.5f)
+ .height(35.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Blue,
+ disabledContainerColor = Color.Gray
+ ),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Text("Attack", color = Color.White, fontSize = 12.sp)
+ }
+ }
+ }
+}
+
+@Composable
+fun PlayerBattleView(
+ battleSystem: ArenaBattleSystem,
+ stage: String,
+ playerName: String,
+ attackAnimationProgress: Float,
+ onAttackClick: () -> Unit,
+ activeCharacter: APIBattleCharacter?,
+ context: android.content.Context?,
+ opponent: APIBattleCharacter?,
+ onSetPendingDamage: (Float, Float) -> Unit,
+ coroutineScope: kotlinx.coroutines.CoroutineScope,
+ hidePlayerAttackSprite: Boolean,
+ selectedBackgroundSet: Int = 0
+) {
+ // 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()
+ ) {
+ // Multi-layer animated battle background
+ MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize(), backgroundSetIndex = selectedBackgroundSet)
+
+ // Top section: HP bar and HP numbers
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ // Health bar and text with background box
+ Box(
+ modifier = getLandscapeBoxModifier(),
+ contentAlignment = getLandscapeAlignment()
+ ) {
+ 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 = getLandscapeFontSize(),
+ 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
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // Player Digimon (left side)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .size(80.dp),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ // 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)
+ 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.playerDodgeProgress
+ val direction = battleSystem.playerDodgeDirection
+
+ if (direction > 0) {
+ // Moving up (negative offset to move UP visually)
+ -(progress * dodgeHeight.value).dp
+ } else {
+ // Moving back down (from negative peak to 0)
+ -((1f - progress) * dodgeHeight.value).dp
+ }
+ } else {
+ 0.dp
+ }
+
+ // Calculate hit effect (slight shake)
+ val hitOffset = if (battleSystem.isPlayerShakeDelayed) {
+ 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
+ .offset(
+ x = hitOffset,
+ y = verticalOffset
+ ),
+ 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) {
+ 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) {
+ 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) {
+ 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
+ }
+ 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
+ )
+ .scale(1f, 1f), // Don't flip enemy attacks on player screen
+ contentScale = ContentScale.Fit
+ )
+ }
+ }
+ }
+ }
+
+
+ }
+}
+
+@Composable
+fun EnemyBattleView(
+ battleSystem: ArenaBattleSystem,
+ stage: String,
+ opponentName: String,
+ attackAnimationProgress: Float,
+ activeCharacter: APIBattleCharacter? = null,
+ playerCharacter: APIBattleCharacter? = null,
+ hideEnemyAttackSprite: Boolean,
+ selectedBackgroundSet: Int = 0
+) {
+ // 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()
+ ) {
+ // Multi-layer animated battle background
+ MultiLayerAnimatedBattleBackground(modifier = Modifier.fillMaxSize(), backgroundSetIndex = selectedBackgroundSet)
+
+ // Top section: Enemy HP bar and HP numbers
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ // Enemy HP bar and text with background box
+ Box(
+ modifier = getLandscapeBoxModifier(),
+ contentAlignment = getLandscapeAlignment()
+ ) {
+ Column(
+ horizontalAlignment = getLandscapeHorizontalAlignment()
+ ) {
+ // Enemy HP bar
+ LinearProgressIndicator(
+ progress = battleSystem.opponentHP / (activeCharacter?.baseHp?.toFloat() ?: 100f),
+ modifier = getLandscapeModifier(),
+ 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 = getLandscapeFontSize(),
+ color = Color.White,
+ style = TextStyle(
+ shadow = Shadow(
+ color = Color.Black,
+ offset = androidx.compose.ui.geometry.Offset(4f, 4f),
+ //blurRadius = 2f
+ )
+ )
+ )
+ }
+ }
+ }
+
+ // Middle section: Enemy Digimon
+ Box(
+ 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
+ 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
+ }
+
+ // Calculate vertical offset for dodge animation
+ val verticalOffset = if (battleSystem.isOpponentDodging) {
+ val dodgeHeight = 30.dp
+ val progress = battleSystem.opponentDodgeProgress
+ val direction = battleSystem.opponentDodgeDirection
+
+ if (direction > 0) {
+ // Moving up (negative offset to move UP visually)
+ -(progress * dodgeHeight.value).dp
+ } else {
+ // Moving back down (from negative peak to 0)
+ -((1f - progress) * dodgeHeight.value).dp
+ }
+ } else {
+ 0.dp
+ }
+
+ // Calculate hit effect (slight shake)
+ val hitOffset = if (battleSystem.isOpponentShakeDelayed) {
+ 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)
+ .offset(
+ x = hitOffset,
+ y = verticalOffset
+ ),
+ 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) {
+ 2 -> true // Player attack on enemy screen
+ 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
+ }
+
+ 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
+ )
+ .scale(-1f, 1f), // Flip player attacks
+ contentScale = ContentScale.Fit
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BattlesScreen() {
+ val TAG = "BattleScreen"
+
+ var currentView by remember { mutableStateOf("main") }
+
+ 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("") }
+ 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"
+ 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),
+ 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_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)
+ )
+
+ 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
+ LaunchedEffect(Unit) {
+ println("BATTLESCREEN: LaunchedEffect triggered - checking sprite files...")
+ val spriteFileManager = SpriteFileManager(context)
+ if (!spriteFileManager.checkSpriteFilesExist()) {
+ println("BATTLESCREEN: Copying sprite files to external storage...")
+ spriteFileManager.copySpriteFilesToExternalStorage()
+ } else {
+ println("BATTLESCREEN: Sprite files already exist in external storage")
+ }
+ }
+
+ 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(
+ onClick = { currentView = "main" }
+ ) {
+ Text("Back")
+ }
+ }
+
+ 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
+ if (currentView != "battle-main" && currentView != "battle-results") {
TopBanner(
text = "Online battles"
)
+ }
}
) { contentPadding ->
Column(
@@ -27,7 +1928,565 @@ fun BattlesScreen() {
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
- Text("Coming soon")
+ when (currentView) {
+ "main" -> {
+ if (showSpriteTester) {
+ when (spriteTesterView) {
+ "entry" -> spriteTesterEntry()
+ "testing" -> spriteTesterTesting()
+ else -> spriteTesterEntry()
+ }
+ } else {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ rookieButton()
+ championButton()
+ ultimateButton()
+ megaButton()
+ /*
+ 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")
+ }
+ */
+ }
+ }
+ }
+
+ "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?.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("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?.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("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?.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("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?.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("HP: ${character.currentHp}/${character.baseHp}")
+ Text("BP: ${character.baseBp}")
+ Text("AP: ${character.baseAp}")
+ }
+
+ backButton()
+ }
+ }
+
+ "battle-main" -> {
+ BattleScreen(
+ stage = currentStage,
+ playerName = activeCharacter?.name ?: "Player",
+ opponentName = selectedOpponent?.name ?: "Opponent",
+ activeCharacter = activeCharacter,
+ opponentCharacter = selectedOpponent,
+ onAttackClick = {
+ // This will be called when the battle is over
+ currentView = "battle-results"
+ },
+ context = context,
+ selectedBackgroundSet = selectedBackgroundSet
+ )
+ }
+
+ "battle-results" -> {
+ var winnerName by remember { mutableStateOf("") }
+ var isWinnerLoaded by remember { mutableStateOf(false) }
+
+ 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 get the winner info
+ RetrofitHelper().getPVPWinner(
+ context,
+ 1,
+ 2,
+ activeCharacter?.name ?: "Player",
+ playerStage,
+ opponentStage,
+ selectedOpponent?.name ?: "Opponent",
+ opponentStage
+ ) { apiResult ->
+ winnerName = apiResult.winner ?: "Unknown"
+ isWinnerLoaded = true
+
+ // Then send the cleanup call
+ RetrofitHelper().getPVPWinner(
+ context,
+ 2,
+ 2,
+ activeCharacter?.name ?: "Player",
+ playerStage,
+ opponentStage,
+ selectedOpponent?.name ?: "Opponent",
+ opponentStage
+ ) { cleanupResult ->
+ println("Cleanup call completed")
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Battle Complete!",
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ 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
+ )
+ }
+ }
+
+ // Exit button
+ Button(
+ onClick = { currentView = "main" },
+ modifier = Modifier.align(Alignment.TopCenter),
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
+ ) {
+ Text("Exit", color = Color.White)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@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 external storage
+ LaunchedEffect(Unit) {
+ try {
+ 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}")
+ 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()) {
+ // 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
+
+ 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
+ )
+ }
}
}
}
+
+// 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,
+ backgroundSetIndex: Int = 0
+) {
+ 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 external 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}")
+ 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}")
+ }
+
+ // 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 (Set ${backgroundSetIndex + 1}): ${middleLayerFile.absolutePath}")
+ } else {
+ println("Middle layer background file not found: ${middleLayerFile.absolutePath}")
+ }
+
+ // 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 (Set ${backgroundSetIndex + 1}): ${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 (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 * speedMultiplier
+ if (backLayerXOffset <= -screenWidth.value) {
+ backLayerXOffset = 0f
+ }
+
+ // Middle layer moves at medium speed
+ middleLayerXOffset -= 1f * speedMultiplier
+ if (middleLayerXOffset <= -screenWidth.value) {
+ middleLayerXOffset = 0f
+ }
+
+ // Front layer moves fastest
+ frontLayerXOffset -= 1.5f * speedMultiplier
+ if (frontLayerXOffset <= -screenWidth.value) {
+ frontLayerXOffset = 0f
+ }
+ }
+ }
+ }
+
+ 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 ->
+ 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 ->
+ 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 ->
+ 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
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