From f0f1d9830e6381803b2f9276f0fd072a5923b9cf Mon Sep 17 00:00:00 2001 From: lightheel Date: Tue, 5 Aug 2025 16:14:17 -0400 Subject: [PATCH] Added dodge and attack hit animations. --- .../vbhelper/battle/ArenaBattleSystem.kt | 187 ++++++++++ .../vbhelper/screens/BattlesScreen.kt | 326 ++++++++++++++++-- 2 files changed, 475 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt index 09999f7..3c4d048 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/ArenaBattleSystem.kt @@ -43,6 +43,46 @@ class ArenaBattleSystem { private var _critBarProgress by mutableStateOf(0) val critBarProgress: Int get() = _critBarProgress + // Dodge animation states + private var _isDodging by mutableStateOf(false) + val isDodging: Boolean get() = _isDodging + + private var _dodgeProgress by mutableStateOf(0f) + val dodgeProgress: Float get() = _dodgeProgress + + private var _dodgeDirection by mutableStateOf(1f) // 1f = up, -1f = down + val dodgeDirection: Float get() = _dodgeDirection + + private var _isHit by mutableStateOf(false) + val isHit: Boolean get() = _isHit + + private var _hitProgress by mutableStateOf(0f) + val hitProgress: Float get() = _hitProgress + + // Separate states for player and opponent + private var _isPlayerDodging by mutableStateOf(false) + val isPlayerDodging: Boolean get() = _isPlayerDodging + + private var _isOpponentDodging by mutableStateOf(false) + val isOpponentDodging: Boolean get() = _isOpponentDodging + + private var _isPlayerHit by mutableStateOf(false) + val isPlayerHit: Boolean get() = _isPlayerHit + + private var _isOpponentHit by mutableStateOf(false) + val isOpponentHit: Boolean get() = _isOpponentHit + + // Counter-attack tracking + private var _shouldCounterAttack by mutableStateOf(false) + val shouldCounterAttack: Boolean get() = _shouldCounterAttack + + private var _counterAttackIsHit by mutableStateOf(false) + val counterAttackIsHit: Boolean get() = _counterAttackIsHit + + // Separate tracking for opponent attack result + private var _opponentAttackIsHit by mutableStateOf(false) + val opponentAttackIsHit: Boolean get() = _opponentAttackIsHit + fun startPlayerAttack() { Log.d(TAG, "Starting player attack") _attackPhase = 1 @@ -121,6 +161,18 @@ class ArenaBattleSystem { _isPlayerAttacking = false _attackIsHit = false _currentView = 0 + _isDodging = false + _dodgeProgress = 0f + _dodgeDirection = 1f + _isHit = false + _hitProgress = 0f + _isPlayerDodging = false + _isOpponentDodging = false + _isPlayerHit = false + _isOpponentHit = false + _shouldCounterAttack = false + _counterAttackIsHit = false + _opponentAttackIsHit = false Log.d(TAG, "Reset attack state") } @@ -137,4 +189,139 @@ class ArenaBattleSystem { _critBarProgress = progress //Log.d(TAG, "Updated crit bar progress: $progress") } + + // Dodge animation methods + fun startDodge() { + _isDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f // Start moving up + Log.d(TAG, "Started dodge animation") + } + + fun setDodgeProgress(progress: Float) { + _dodgeProgress = progress + } + + fun setDodgeDirection(direction: Float) { + _dodgeDirection = direction + } + + fun endDodge() { + _isDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended dodge animation") + } + + // Hit animation methods + fun startHit() { + _isHit = true + _hitProgress = 0f + Log.d(TAG, "Started hit animation") + } + + fun setHitProgress(progress: Float) { + _hitProgress = progress + } + + fun endHit() { + _isHit = false + _hitProgress = 0f + Log.d(TAG, "Ended hit animation") + } + + // Player-specific dodge methods + fun startPlayerDodge() { + _isPlayerDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f + Log.d(TAG, "Started player dodge animation") + } + + fun endPlayerDodge() { + _isPlayerDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended player dodge animation") + } + + // Opponent-specific dodge methods + fun startOpponentDodge() { + _isOpponentDodging = true + _dodgeProgress = 0f + _dodgeDirection = 1f + Log.d(TAG, "Started opponent dodge animation") + } + + fun endOpponentDodge() { + _isOpponentDodging = false + _dodgeProgress = 0f + Log.d(TAG, "Ended opponent dodge animation") + } + + // Player-specific hit methods + fun startPlayerHit() { + _isPlayerHit = true + _hitProgress = 0f + Log.d(TAG, "Started player hit animation") + } + + fun endPlayerHit() { + _isPlayerHit = false + _hitProgress = 0f + Log.d(TAG, "Ended player hit animation") + } + + // Opponent-specific hit methods + fun startOpponentHit() { + _isOpponentHit = true + _hitProgress = 0f + Log.d(TAG, "Started opponent hit animation") + } + + fun endOpponentHit() { + _isOpponentHit = false + _hitProgress = 0f + Log.d(TAG, "Ended opponent hit animation") + } + + // Combined method to handle attack result + fun handleAttackResult(isHit: Boolean) { + _attackIsHit = isHit + if (isHit) { + // Player attack hit - opponent gets hit + startOpponentHit() + } else { + // Player attack missed - opponent dodges + startOpponentDodge() + } + Log.d(TAG, "Handled player attack result: ${if (isHit) "HIT" else "DODGE"}") + } + + // Method to handle opponent attack result + fun handleOpponentAttackResult(isHit: Boolean) { + _opponentAttackIsHit = isHit + if (isHit) { + // Opponent attack hit - player gets hit + startPlayerHit() + } else { + // Opponent attack missed - player dodges + startPlayerDodge() + } + Log.d(TAG, "Handled opponent attack result: ${if (isHit) "HIT" else "DODGE"}") + } + + // Counter-attack methods + fun setupCounterAttack(isHit: Boolean) { + _shouldCounterAttack = true + _counterAttackIsHit = isHit + Log.d(TAG, "Setup counter-attack: ${if (isHit) "HIT" else "DODGE"}, isHit=$isHit") + } + + fun startCounterAttack() { + _attackPhase = 3 + _attackProgress = 0f + _isPlayerAttacking = false + _currentView = 1 + _opponentAttackIsHit = _counterAttackIsHit + Log.d(TAG, "Started counter-attack with opponentAttackIsHit=$_opponentAttackIsHit, counterAttackIsHit=$_counterAttackIsHit") + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt index c41ec2b..03a9b61 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt @@ -37,6 +37,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.material3.ButtonDefaults @@ -52,6 +54,8 @@ import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import kotlin.math.sin +import kotlin.math.PI @Composable @@ -65,6 +69,7 @@ fun BattleScreen( context: android.content.Context? = null ) { val battleSystem = remember { ArenaBattleSystem() } + val coroutineScope = rememberCoroutineScope() // Initialize HP when battle starts LaunchedEffect(activeCharacter, opponentCharacter) { @@ -110,14 +115,36 @@ fun BattleScreen( while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) + + // Trigger animation when attack reaches the opponent (around 55% progress for opponent dodge) + if (progress >= 0.55f && !battleSystem.isOpponentHit && !battleSystem.isOpponentDodging) { + if (battleSystem.attackIsHit) { + // Player attack hits opponent + println("Player attack hits opponent at progress $progress") + battleSystem.startOpponentHit() + } else { + // Player attack misses, opponent dodges + println("Player attack misses, opponent dodges at progress $progress") + battleSystem.startOpponentDodge() + } + } + delay(16) // 60 FPS } - println("Phase 2 completed, applying damage and starting opponent attack") - // Apply player's damage and start opponent attack - battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) - pendingOpponentDamage = 0f - delay(500) - battleSystem.startOpponentAttack() + println("Phase 2 completed, applying damage and starting opponent attack") + // Apply player's damage + battleSystem.completeAttackAnimation(opponentDamage = pendingOpponentDamage) + pendingOpponentDamage = 0f + delay(500) + + // Check if there's a counter-attack set up + if (battleSystem.shouldCounterAttack) { + println("Starting counter-attack") + battleSystem.startCounterAttack() + } else { + // Normal opponent attack + battleSystem.startOpponentAttack() + } } 3 -> { // Phase 3: Opponent attack on opponent screen @@ -140,14 +167,31 @@ fun BattleScreen( while (progress < 1f) { progress += 0.016f // 60 FPS battleSystem.setAttackProgress(progress) + + // Trigger animation when attack reaches the player (around 75% progress - earlier for better timing) + if (progress >= 0.75f && !battleSystem.isPlayerHit && !battleSystem.isPlayerDodging) { + println("Phase 4: Checking player animation at progress $progress, opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, shouldCounterAttack=${battleSystem.shouldCounterAttack}, counterAttackIsHit=${battleSystem.counterAttackIsHit}") + println("Phase 4: Player animation decision - opponentAttackIsHit=${battleSystem.opponentAttackIsHit}, will ${if (battleSystem.opponentAttackIsHit) "HIT" else "DODGE"}") + println("Phase 4: Full debug - attackPhase=${battleSystem.attackPhase}, isPlayerAttacking=${battleSystem.isPlayerAttacking}") + if (battleSystem.opponentAttackIsHit) { + // Opponent attack hits player + println("Opponent attack hits player at progress $progress") + battleSystem.startPlayerHit() + } else { + // Opponent attack misses, player dodges + println("Opponent attack misses, player dodges at progress $progress") + battleSystem.startPlayerDodge() + } + } + delay(16) // 60 FPS } - println("Phase 4 completed, applying damage and resetting") - // Apply opponent's damage and reset - battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) - pendingPlayerDamage = 0f - battleSystem.resetAttackState() - battleSystem.enableAttackButton() + println("Phase 4 completed, applying damage and resetting") + // Apply opponent's damage and reset + battleSystem.completeAttackAnimation(playerDamage = pendingPlayerDamage) + pendingPlayerDamage = 0f + battleSystem.resetAttackState() + battleSystem.enableAttackButton() // Check if battle is over if (battleSystem.checkBattleOver()) { @@ -158,6 +202,112 @@ fun BattleScreen( } } + // Player dodge animation + LaunchedEffect(battleSystem.isPlayerDodging) { + if (battleSystem.isPlayerDodging) { + println("Starting player dodge animation") + var dodgeProgress = 0f + var dodgeDirection = 1f // Start moving up + + // Move up + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f // Faster dodge movement + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) // 60 FPS + } + + // Wait at the top + delay(200) + + // Move back down + dodgeDirection = -1f + dodgeProgress = 0f + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) + } + + battleSystem.endPlayerDodge() + println("Player dodge animation completed") + } + } + + // Opponent dodge animation + LaunchedEffect(battleSystem.isOpponentDodging) { + if (battleSystem.isOpponentDodging) { + println("Starting opponent dodge animation") + var dodgeProgress = 0f + var dodgeDirection = 1f // Start moving up + + // Move up + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f // Faster dodge movement + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) // 60 FPS + } + + // Wait at the top + delay(200) + + // Move back down + dodgeDirection = -1f + dodgeProgress = 0f + while (dodgeProgress < 1f) { + dodgeProgress += 0.05f + battleSystem.setDodgeProgress(dodgeProgress) + battleSystem.setDodgeDirection(dodgeDirection) + delay(16) + } + + battleSystem.endOpponentDodge() + println("Opponent dodge animation completed") + } + } + + // Player hit animation + LaunchedEffect(battleSystem.isPlayerHit) { + if (battleSystem.isPlayerHit) { + println("Starting player hit animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endPlayerHit() + println("Player hit animation completed") + } + } + + // Opponent hit animation + LaunchedEffect(battleSystem.isOpponentHit) { + if (battleSystem.isOpponentHit) { + println("Starting opponent hit animation") + var hitProgress = 0f + + // Quick hit effect + while (hitProgress < 1f) { + hitProgress += 0.1f // Fast hit effect + battleSystem.setHitProgress(hitProgress) + delay(16) + } + + delay(100) // Brief pause + + battleSystem.endOpponentHit() + println("Opponent hit animation completed") + } + } + Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -179,7 +329,8 @@ fun BattleScreen( onSetPendingDamage = { playerDamage, opponentDamage -> pendingPlayerDamage = playerDamage pendingOpponentDamage = opponentDamage - } + }, + coroutineScope = coroutineScope ) } 1 -> { @@ -207,12 +358,14 @@ fun PlayerBattleView( activeCharacter: APIBattleCharacter?, context: android.content.Context?, opponent: APIBattleCharacter?, - onSetPendingDamage: (Float, Float) -> Unit + onSetPendingDamage: (Float, Float) -> Unit, + coroutineScope: kotlinx.coroutines.CoroutineScope ) { // Track previous character ID to detect transitions var previousCharacterId by remember { mutableStateOf(null) } var previousAttackPhase by remember { mutableStateOf(null) } var isTransitioning by remember { mutableStateOf(false) } + var lastApiResult by remember { mutableStateOf(null) } Box( modifier = Modifier.fillMaxSize() @@ -238,6 +391,16 @@ fun PlayerBattleView( Spacer(modifier = Modifier.height(16.dp)) + // Debug display + if (lastApiResult != null) { + Text( + text = "Debug: state=${lastApiResult!!.state}, playerAttackHit=${lastApiResult!!.playerAttackHit}, opponentDamage=${lastApiResult!!.opponentAttackDamage}", + color = Color.Red, + fontSize = 10.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + } + // Health bar LinearProgressIndicator( progress = battleSystem.playerHP / (activeCharacter?.baseHp?.toFloat() ?: 100f), @@ -271,20 +434,54 @@ fun PlayerBattleView( contentAlignment = Alignment.CenterStart ) { // Determine animation type based on battle state - val animationType = when (battleSystem.attackPhase) { - 1 -> DigimonAnimationType.ATTACK // Player attack on player screen - 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen - 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen - 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen + val animationType = when { + battleSystem.isPlayerDodging -> DigimonAnimationType.WALK // Use walk animation for dodge + battleSystem.isPlayerHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.attackPhase == 1 -> DigimonAnimationType.ATTACK // Player attack on player screen + battleSystem.attackPhase == 2 -> DigimonAnimationType.ATTACK // Player attack on opponent screen + battleSystem.attackPhase == 3 -> DigimonAnimationType.IDLE // Opponent attack on opponent screen + battleSystem.attackPhase == 4 -> DigimonAnimationType.IDLE // Opponent attack on player screen else -> DigimonAnimationType.IDLE } + // Calculate vertical offset for dodge animation + val verticalOffset = if (battleSystem.isPlayerDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.dodgeProgress + val direction = battleSystem.dodgeDirection + + if (direction > 0) { + // Moving up + (progress * dodgeHeight.value).dp + } else { + // Moving back down + ((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect (slight shake) + val hitOffset = if (battleSystem.isPlayerHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + // Simple shake effect without complex math + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + AnimatedSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", animationType = animationType, modifier = Modifier .size(80.dp) - .scale(-1f, 1f), // Flip player Digimon horizontally + .scale(-1f, 1f) // Flip player Digimon horizontally + .offset( + x = hitOffset, + y = verticalOffset + ), contentScale = ContentScale.Fit, reloadMappings = false ) @@ -407,6 +604,7 @@ fun PlayerBattleView( ) { apiResult -> // Handle API response here println("API Result: $apiResult") + lastApiResult = apiResult // Store for debug display // Update HP based on API response when (apiResult.state) { @@ -414,18 +612,35 @@ fun PlayerBattleView( // Match is still ongoing - update HP and continue println("Round ${apiResult.currentRound}: Player HP=${apiResult.playerHP}, Opponent HP=${apiResult.opponentHP}") - // Set pending damage based on API result - if (apiResult.playerAttackHit) { - // Player attack hit - enemy takes damage at end of player animation - println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") - onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage - battleSystem.setAttackHitState(true) - } else { - // Player attack missed - enemy counter-attacks and player takes damage - println("Player attack missed! Enemy counter-attacks and player takes ${apiResult.opponentAttackDamage} damage") - onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage - battleSystem.setAttackHitState(false) - } + // Set pending damage based on API result + if (apiResult.playerAttackDamage > 0) { + // Player attack hit - enemy takes damage at end of player animation + println("Player attack hit! Enemy will take ${apiResult.playerAttackDamage} damage") + onSetPendingDamage(0f, apiResult.playerAttackDamage.toFloat()) // Opponent takes damage + battleSystem.setAttackHitState(true) + } else { + // Player attack missed - enemy counter-attacks + println("Player attack missed! Enemy counter-attacks") + battleSystem.setAttackHitState(false) + // Set up counter-attack - determine if it hits based on API result + val counterAttackHits = apiResult.opponentAttackDamage > 0 + println("Setting up counter-attack: counterAttackHits=$counterAttackHits, opponentAttackDamage=${apiResult.opponentAttackDamage}") + println("Full API response: status=${apiResult.status}, state=${apiResult.state}, playerAttackHit=${apiResult.playerAttackHit}, playerAttackDamage=${apiResult.playerAttackDamage}, opponentAttackDamage=${apiResult.opponentAttackDamage}, playerHP=${apiResult.playerHP}, opponentHP=${apiResult.opponentHP}") + println("DEBUG: Using playerAttackDamage > 0 instead of playerAttackHit for hit detection") + + // Use opponentAttackDamage to determine counter-attack hit + val finalCounterAttackHits = counterAttackHits + println("Using opponentAttackDamage > 0 for counter-attack: $finalCounterAttackHits") + + if (finalCounterAttackHits) { + println("Counter-attack hits! Player takes ${apiResult.opponentAttackDamage} damage") + onSetPendingDamage(apiResult.opponentAttackDamage.toFloat(), 0f) // Player takes damage + } else { + println("Counter-attack misses! Player dodges") + onSetPendingDamage(0f, 0f) // No damage + } + battleSystem.setupCounterAttack(finalCounterAttackHits) + } } 2 -> { // Match is over - transition to results screen @@ -515,18 +730,53 @@ fun OpponentBattleView( contentAlignment = Alignment.CenterEnd ) { // Determine animation type based on battle state - val animationType = when (battleSystem.attackPhase) { - 1 -> DigimonAnimationType.IDLE // Player attack on player screen - 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen - 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen - 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen + val animationType = when { + battleSystem.isOpponentDodging -> DigimonAnimationType.WALK // Use walk animation for dodge + battleSystem.isOpponentHit -> DigimonAnimationType.SLEEP // Use sleep animation for hit effect (injured sprite) + battleSystem.attackPhase == 1 -> DigimonAnimationType.IDLE // Player attack on player screen + battleSystem.attackPhase == 2 -> DigimonAnimationType.IDLE // Player attack on opponent screen + battleSystem.attackPhase == 3 -> DigimonAnimationType.ATTACK // Opponent attack on opponent screen + battleSystem.attackPhase == 4 -> DigimonAnimationType.ATTACK // Opponent attack on player screen else -> DigimonAnimationType.IDLE } + // Calculate vertical offset for dodge animation + val verticalOffset = if (battleSystem.isOpponentDodging) { + val dodgeHeight = 30.dp + val progress = battleSystem.dodgeProgress + val direction = battleSystem.dodgeDirection + + if (direction > 0) { + // Moving up + (progress * dodgeHeight.value).dp + } else { + // Moving back down + ((1f - progress) * dodgeHeight.value).dp + } + } else { + 0.dp + } + + // Calculate hit effect (slight shake) + val hitOffset = if (battleSystem.isOpponentHit) { + val shakeAmount = 5.dp + val progress = battleSystem.hitProgress + // Simple shake effect without complex math + val shake = if (progress < 0.5f) progress * 2f else (1f - progress) * 2f + (shake * shakeAmount.value).dp + } else { + 0.dp + } + AnimatedSpriteImage( characterId = activeCharacter?.charaId ?: "dim011_mon01", animationType = animationType, - modifier = Modifier.size(80.dp), + modifier = Modifier + .size(80.dp) + .offset( + x = hitOffset, + y = verticalOffset + ), contentScale = ContentScale.Fit, reloadMappings = false )