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