From d833a89c170c7540a2db4b98a6b970bc0d2a6e0a Mon Sep 17 00:00:00 2001 From: lightheel Date: Wed, 6 Aug 2025 18:03:03 -0400 Subject: [PATCH] Added hit sprites. --- .../vbhelper/battle/HitEffectComposables.kt | 103 ++++++++++ .../vbhelper/battle/HitEffectSpriteManager.kt | 176 ++++++++++++++++++ .../vbhelper/screens/BattlesScreen.kt | 51 +++++ 3 files changed, 330 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt new file mode 100644 index 0000000..7ccf1c6 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectComposables.kt @@ -0,0 +1,103 @@ +package com.github.nacabaro.vbhelper.battle + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun HitEffectOverlay( + isVisible: Boolean, + modifier: Modifier = Modifier, + isPlayerScreen: Boolean = false, + onAnimationComplete: () -> Unit = {} +) { + if (!isVisible) return + + val context = LocalContext.current + val hitEffectManager = remember { HitEffectSpriteManager(context) } + val coroutineScope = rememberCoroutineScope() + + var currentFrame by remember { mutableStateOf(0) } + var currentSprite by remember { mutableStateOf(null) } + var animationProgress by remember { mutableStateOf(0f) } + var scale by remember { mutableStateOf(0.5f) } + var alpha by remember { mutableStateOf(1f) } + + LaunchedEffect(isVisible) { + if (isVisible) { + println("DEBUG: Starting hit effect animation") + + // Randomly choose between hit_01, hit_02, and hit_02_white + val hitSpriteName = when (kotlin.random.Random.nextInt(3)) { + 0 -> "hit_01" + 1 -> "hit_02" + else -> "hit_02_white" + } + currentSprite = hitEffectManager.loadHitSprite(hitSpriteName) + + if (currentSprite != null) { + // Animate the hit effect + animationProgress = 0f + scale = 0.5f + alpha = 1f + + // Scale up animation - slowed down + while (scale < 1.2f) { + scale += 0.05f // Reduced from 0.1f to 0.05f + delay(32) // Increased from 16ms to 32ms + } + + // Hold for a moment - increased duration + delay(300) // Increased from 100ms to 300ms + + // Fade out - slowed down + while (alpha > 0f) { + alpha -= 0.03f // Reduced from 0.05f to 0.03f + delay(32) // Increased from 16ms to 32ms + } + + println("DEBUG: Hit effect animation completed") + onAnimationComplete() + } else { + println("DEBUG: Failed to load hit sprite") + onAnimationComplete() + } + } + } + + currentSprite?.let { sprite -> + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + bitmap = sprite.asImageBitmap(), + contentDescription = "Hit Effect", + modifier = Modifier + .size((sprite.width * scale).dp, (sprite.height * scale).dp) + .offset( + x = if (isPlayerScreen) { + // On player screen, position further to the left + (-sprite.width * scale / 2 - 100).dp + } else { + // On enemy screen, position further to the right + (-sprite.width * scale / 2 + 150).dp + }, + y = (-sprite.height * scale / 2 + 40).dp // Position lower on screen (was -60, now +40) + ), + contentScale = ContentScale.Fit + ) + } + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt new file mode 100644 index 0000000..33d2189 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/battle/HitEffectSpriteManager.kt @@ -0,0 +1,176 @@ +package com.github.nacabaro.vbhelper.battle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Rect +import java.io.File + +class HitEffectSpriteManager(private val context: Context) { + private val spriteCache = mutableMapOf() + + // Base directory where hit effect sprites are stored + private val hitSpritesDir = File(context.filesDir, "battle_sprites/extracted_hit_sprites") + + /** + * Load a hit sprite (hit_01.png, hit_02.png, hit_02_white.png) + * @param spriteName The sprite name (e.g., "hit_01", "hit_02", "hit_02_white") + * @return Bitmap of the hit sprite, or null if not found + */ + fun loadHitSprite(spriteName: String): Bitmap? { + val cacheKey = "hit_$spriteName" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + try { + val spriteFile = File(hitSpritesDir, "$spriteName.png") + + if (!spriteFile.exists()) { + println("Hit sprite file not found: ${spriteFile.absolutePath}") + return null + } + + val bitmap = BitmapFactory.decodeFile(spriteFile.absolutePath) + if (bitmap == null) { + println("Failed to decode hit sprite file: ${spriteFile.absolutePath}") + return null + } + + println("Successfully loaded hit sprite: $spriteName.png (${bitmap.width}x${bitmap.height})") + + // Cache the result + spriteCache[cacheKey] = bitmap + + return bitmap + + } catch (e: Exception) { + println("Error loading hit sprite: ${e.message}") + e.printStackTrace() + return null + } + } + + /** + * Load a damage effect sprite from spritesheet + * @param spritesheetName The spritesheet name (e.g., "dmg_ef1", "dmg_ef2") + * @param frameIndex The frame index (0-3 for dmg_ef1 and dmg_ef2, 0 for dmg_ef3) + * @return Bitmap of the damage effect frame, or null if not found + */ + fun loadDamageEffectSprite(spritesheetName: String, frameIndex: Int = 0): Bitmap? { + val cacheKey = "dmg_${spritesheetName}_frame_${frameIndex}" + + // Check cache first + if (spriteCache.containsKey(cacheKey)) { + return spriteCache[cacheKey] + } + + try { + val spritesheetFile = File(hitSpritesDir, "$spritesheetName.png") + + if (!spritesheetFile.exists()) { + println("Damage effect spritesheet not found: ${spritesheetFile.absolutePath}") + return null + } + + val spritesheet = BitmapFactory.decodeFile(spritesheetFile.absolutePath) + if (spritesheet == null) { + println("Failed to decode damage effect spritesheet: ${spritesheetFile.absolutePath}") + return null + } + + // Extract frame from spritesheet + val frameBitmap = when (spritesheetName) { + "dmg_ef1", "dmg_ef2" -> { + // These are 2x2 spritesheets (4 frames) + val frameWidth = spritesheet.width / 2 + val frameHeight = spritesheet.height / 2 + val row = frameIndex / 2 + val col = frameIndex % 2 + val x = col * frameWidth + val y = row * frameHeight + Bitmap.createBitmap(spritesheet, x, y, frameWidth, frameHeight) + } + "dmg_ef3" -> { + // This is a single sprite + spritesheet + } + else -> { + println("Unknown spritesheet name: $spritesheetName") + return null + } + } + + println("Successfully loaded damage effect frame: $spritesheetName frame $frameIndex (${frameBitmap.width}x${frameBitmap.height})") + + // Cache the result + spriteCache[cacheKey] = frameBitmap + + return frameBitmap + + } catch (e: Exception) { + println("Error loading damage effect sprite: ${e.message}") + e.printStackTrace() + return null + } + } + + /** + * Get available hit sprite names + * @return List of available hit sprite names + */ + fun getAvailableHitSprites(): List { + try { + if (!hitSpritesDir.exists()) { + return emptyList() + } + + val hitFiles = hitSpritesDir.listFiles { file -> + file.name.startsWith("hit_") && file.name.endsWith(".png") + } ?: emptyArray() + + return hitFiles.map { file -> + file.name.substringBefore(".png") + }.sorted() + + } catch (e: Exception) { + println("Error getting available hit sprites: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Get available damage effect spritesheet names + * @return List of available damage effect spritesheet names + */ + fun getAvailableDamageEffectSpritesheets(): List { + try { + if (!hitSpritesDir.exists()) { + return emptyList() + } + + val dmgFiles = hitSpritesDir.listFiles { file -> + file.name.startsWith("dmg_ef") && file.name.endsWith(".png") + } ?: emptyArray() + + return dmgFiles.map { file -> + file.name.substringBefore(".png") + }.sorted() + + } catch (e: Exception) { + println("Error getting available damage effect spritesheets: ${e.message}") + e.printStackTrace() + return emptyList() + } + } + + /** + * Clear the sprite cache + */ + fun clearCache() { + spriteCache.clear() + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt index c78b0db..065308a 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/BattlesScreen.kt @@ -52,6 +52,7 @@ import com.github.nacabaro.vbhelper.battle.SpriteFileManager import com.github.nacabaro.vbhelper.battle.ArenaBattleSystem import com.github.nacabaro.vbhelper.battle.DigimonAnimationType import com.github.nacabaro.vbhelper.battle.AnimatedSpriteImage +import com.github.nacabaro.vbhelper.battle.HitEffectOverlay import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import kotlin.math.sin @@ -162,6 +163,20 @@ fun BattleScreen( var playerDamageValue by remember { mutableStateOf(0) } var opponentDamageValue by remember { mutableStateOf(0) } + // Hit effect animation state + var showPlayerHitEffect by remember { mutableStateOf(false) } + var showOpponentHitEffect by remember { mutableStateOf(false) } + + // Reset hit effect states when attack phase returns to idle + LaunchedEffect(battleSystem.attackPhase) { + if (battleSystem.attackPhase == 0) { + // Reset hit effect states when returning to idle + showPlayerHitEffect = false + showOpponentHitEffect = false + println("DEBUG: Reset hit effect states - attack phase returned to idle") + } + } + // Critical bar timer LaunchedEffect(Unit) { while (true) { @@ -190,6 +205,7 @@ fun BattleScreen( 2 -> { // Phase 2: Player attack on enemy screen println("Starting Phase 2: Player attack on enemy screen") + println("DEBUG: Phase 2 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(2) // Enemy screen var progress = 0f while (progress < 1f) { @@ -202,6 +218,9 @@ fun BattleScreen( // Player attack hits enemy println("Player attack hits enemy at progress $progress") battleSystem.startOpponentHit() + // Show hit effect and damage effect + println("DEBUG: Setting showOpponentHitEffect = true (player attack hits enemy)") + showOpponentHitEffect = true // Show damage number when attack reaches enemy if (pendingOpponentDamage > 0) { showOpponentDamageNumber = true @@ -241,6 +260,7 @@ fun BattleScreen( 3 -> { // Phase 3: Enemy attack on player screen println("Starting Phase 3: Enemy attack on player screen") + println("DEBUG: Phase 3 - showPlayerHitEffect=$showPlayerHitEffect, showOpponentHitEffect=$showOpponentHitEffect") battleSystem.switchToView(1) // Player screen var progress = 0f while (progress < 1f) { @@ -255,6 +275,9 @@ fun BattleScreen( // Enemy attack hits player println("Enemy attack hits player at progress $progress") battleSystem.startPlayerHit() + // Show hit effect and damage effect + println("DEBUG: Setting showPlayerHitEffect = true (enemy attack hits player)") + showPlayerHitEffect = true // Show damage number when attack reaches player if (pendingPlayerDamage > 0) { showPlayerDamageNumber = true @@ -493,6 +516,20 @@ fun BattleScreen( //.background(Color.Yellow.copy(alpha = 0.3f)) // Debug background ) + // Player hit effects + HitEffectOverlay( + isVisible = showPlayerHitEffect, + modifier = Modifier.fillMaxSize(), + isPlayerScreen = true, + onAnimationComplete = { + println("DEBUG: Player hit effect animation completed, setting showPlayerHitEffect = false") + showPlayerHitEffect = false + println("DEBUG: Player hit effect animation completed") + } + ) + + + // Debug text overlay /* Text( @@ -516,6 +553,20 @@ fun BattleScreen( .align(Alignment.Center) .offset(y = (-50).dp) ) + + // Enemy hit effects + HitEffectOverlay( + isVisible = showOpponentHitEffect, + modifier = Modifier.fillMaxSize(), + isPlayerScreen = false, + onAnimationComplete = { + println("DEBUG: Enemy hit effect animation completed, setting showOpponentHitEffect = false") + showOpponentHitEffect = false + println("DEBUG: Enemy hit effect animation completed") + } + ) + + } } }