Merge pull request #51 from lightheel/main

Merging battle system into repo.
This commit is contained in:
Nacho 2026-05-24 21:18:00 +02:00 committed by GitHub
commit 1fffab25bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 5454 additions and 5 deletions

7
.gitignore vendored
View File

@ -10,3 +10,10 @@ 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
app/src/main/assets/extracted_audio
API-ACR122USAM-2.01.pdf

View File

@ -103,4 +103,19 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
// HTTP request logging
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
}

View File

@ -4,6 +4,11 @@
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
android:name=".di.VBHelper"
@ -15,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VBHelper"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@ -26,6 +32,19 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vbhelper" android:host="auth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="localhost" android:port="8080" android:pathPrefix="/authenticate" />
<data android:scheme="http" android:host="127.0.0.1" android:port="8080" android:pathPrefix="/authenticate" />
</intent-filter>
</activity>
</application>

View File

@ -0,0 +1,14 @@
package com.github.nacabaro.vbhelper.battle
data class APIBattleCharacter(
val name: String,
val namekey: String,
val charaId: String,
val stage: Int,
val attribute: Int,
val baseHp: Int,
val currentHp: Int,
val baseBp: Float,
val baseAp: Float,
val displayName: String? = null
)

View File

@ -0,0 +1,82 @@
package com.github.nacabaro.vbhelper.battle
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
@Composable
fun AnimatedSpriteImage(
characterId: String,
animationType: DigimonAnimationType = DigimonAnimationType.IDLE,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
reloadMappings: Boolean = false,
animationOffset: Long = 0L // New parameter for offsetting animation timing
) {
val context = LocalContext.current
val spriteManager = remember { IndividualSpriteManager(context) }
// Calculate frame offset based on animation offset
// 750ms is the idle animation duration, so we calculate how many frames to offset
val frameOffset = if (animationOffset > 0L) {
// Convert time offset to frame offset (2 frames per cycle, 750ms per frame)
((animationOffset / 750L) * 2).toInt()
} else {
0
}
val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context, frameOffset, animationOffset) }
val coroutineScope = rememberCoroutineScope()
var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(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()
bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber)
if (bitmap == null) {
println("Failed to load animated sprite frame: $frameNumber for character: $characterId")
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Animated Sprite: $characterId - ${animationStateMachine.currentAnimation}",
modifier = modifier,
contentScale = contentScale
)
}
}

View File

@ -0,0 +1,385 @@
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() {
_attackPhase = 1
_attackProgress = 0f
_isPlayerAttacking = true
_isAttackButtonEnabled = false
_currentView = 0
}
fun startOpponentAttack() {
_attackPhase = 3
_attackProgress = 0f
_isPlayerAttacking = false
_currentView = 1
}
fun advanceAttackPhase() {
_attackPhase++
_attackProgress = 0f
}
fun setAttackProgress(progress: Float) {
_attackProgress = progress
}
fun setAttackHitState(isHit: Boolean) {
_attackIsHit = isHit
}
fun switchToView(view: Int) {
_currentView = view
}
fun enableAttackButton() {
_isAttackButtonEnabled = true
}
fun applyDamage(isPlayer: Boolean, damage: Float) {
if (isPlayer) {
_playerHP = (_playerHP - damage).coerceAtLeast(0f)
} else {
_opponentHP = (_opponentHP - damage).coerceAtLeast(0f)
}
}
fun updateHPFromAPI(playerHP: Float, opponentHP: Float) {
_playerHP = playerHP
_opponentHP = opponentHP
}
fun initializeHP(playerHP: Float, opponentHP: Float) {
_playerHP = playerHP
_opponentHP = opponentHP
}
fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) {
if (playerDamage > 0f) {
applyDamage(true, playerDamage)
}
if (opponentDamage > 0f) {
applyDamage(false, 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
}
fun checkBattleOver(): Boolean {
return _playerHP <= 0f || _opponentHP <= 0f
}
fun endBattle() {
_isBattleOver = true
}
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
}
fun setDodgeProgress(progress: Float) {
_dodgeProgress = progress
}
fun setDodgeDirection(direction: Float) {
_dodgeDirection = direction
}
fun endDodge() {
_isDodging = false
_dodgeProgress = 0f
}
// Hit animation methods
fun startHit() {
_isHit = true
_hitProgress = 0f
}
fun setHitProgress(progress: Float) {
_hitProgress = progress
}
fun endHit() {
_isHit = false
_hitProgress = 0f
}
// Player-specific dodge methods
fun startPlayerDodge() {
_isPlayerDodging = true
_playerDodgeProgress = 0f
_playerDodgeDirection = 1f
}
fun endPlayerDodge() {
_isPlayerDodging = false
_playerDodgeProgress = 0f
}
fun setPlayerDodgeProgress(progress: Float) {
_playerDodgeProgress = progress
}
fun setPlayerDodgeDirection(direction: Float) {
_playerDodgeDirection = direction
}
// Opponent-specific dodge methods
fun startOpponentDodge() {
_isOpponentDodging = true
_opponentDodgeProgress = 0f
_opponentDodgeDirection = 1f
}
fun endOpponentDodge() {
_isOpponentDodging = false
_opponentDodgeProgress = 0f
}
fun setOpponentDodgeProgress(progress: Float) {
_opponentDodgeProgress = progress
}
fun setOpponentDodgeDirection(direction: Float) {
_opponentDodgeDirection = direction
}
// Player-specific hit methods
fun startPlayerHit() {
_isPlayerHit = true
_hitProgress = 0f
}
fun startPlayerHitDelayed() {
_isPlayerHitDelayed = true
}
fun endPlayerHit() {
_isPlayerHit = false
_hitProgress = 0f
}
fun endPlayerHitDelayed() {
_isPlayerHitDelayed = false
}
// Opponent-specific hit methods
fun startOpponentHit() {
_isOpponentHit = true
_hitProgress = 0f
}
fun startOpponentHitDelayed() {
_isOpponentHitDelayed = true
}
fun endOpponentHit() {
_isOpponentHit = false
_hitProgress = 0f
}
fun endOpponentHitDelayed() {
_isOpponentHitDelayed = false
}
// Delayed shake methods
fun startPlayerShakeDelayed() {
_isPlayerShakeDelayed = true
}
fun endPlayerShakeDelayed() {
_isPlayerShakeDelayed = false
}
fun startOpponentShakeDelayed() {
_isOpponentShakeDelayed = true
}
fun endOpponentShakeDelayed() {
_isOpponentShakeDelayed = false
}
// 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()
}
}
// 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()
}
}
// Counter-attack methods
fun setupCounterAttack(isHit: Boolean) {
_shouldCounterAttack = true
_counterAttackIsHit = isHit
}
fun startCounterAttack() {
_attackPhase = 3
_attackProgress = 0f
_isPlayerAttacking = false
_currentView = 1
_opponentAttackIsHit = _counterAttackIsHit
}
}

View File

@ -0,0 +1,151 @@
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<String>
)
class AttackSpriteManager(private val context: Context) {
private val gson = Gson()
private val characterDataCache = mutableMapOf<String, CharacterData>()
// Get the external storage directory for attack sprites
private fun getAttackTexturesBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites/extracted_atksprites")
}
fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? {
println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge")
try {
// Get character data
val characterData = getCharacterData(characterId) ?: return null
// Determine which attack file to use
val attackFileName = if (isLarge) {
characterData.laugeFileName
} else {
characterData.smalefilename
}
// Skip if no attack file
if (attackFileName == "0") {
println("AttackSpriteManager: Skipping attack file (filename is '0')")
return null
}
// Load the attack sprite from external storage
val attackFile = File(getAttackTexturesBaseDir(), "$attackFileName.png")
return if (attackFile.exists()) {
val bitmap = BitmapFactory.decodeFile(attackFile.absolutePath)
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? {
// Check cache first
if (characterDataCache.containsKey(characterId)) {
return characterDataCache[characterId]
}
try {
// Load character data from JSON file in external storage
val externalDir = android.os.Environment.getExternalStorageDirectory()
val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json")
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()
// 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: "<UnknownObject<Character> 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
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
return characterData
} catch (e: Exception) {
println("AttackSpriteManager: Exception in getCharacterData: ${e.message}")
e.printStackTrace()
return null
}
}
}

View File

@ -0,0 +1,31 @@
package com.github.nacabaro.vbhelper.battle
import okhttp3.Interceptor
import okhttp3.Response
/**
* OkHttp interceptor that adds Authorization header to API requests.
* Skips adding header for auth endpoints.
*/
class AuthInterceptor(private val token: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// Skip adding auth header for auth endpoints
if (originalRequest.url.encodedPath.startsWith("/api/auth")) {
return chain.proceed(originalRequest)
}
// Add authentication header for game endpoints
// Use X-Session-Token header (preferred) or Authorization: Bearer
val authenticatedRequest = originalRequest.newBuilder()
.header("X-Session-Token", token)
.build()
// Debug: Log which header is being used (first few chars of token for security)
val tokenPreview = if (token.length > 8) "${token.take(4)}...${token.takeLast(4)}" else "***"
println("AuthInterceptor: Adding X-Session-Token header (token: $tokenPreview)")
return chain.proceed(authenticatedRequest)
}
}

View File

@ -0,0 +1,14 @@
package com.github.nacabaro.vbhelper.battle
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface AuthService {
@POST("api/auth/validate")
fun validate(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
@POST("api/auth/login")
fun login(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
}

View File

@ -0,0 +1,6 @@
package com.github.nacabaro.vbhelper.battle
data class AuthenticateRequest(
val userToken: String
)

View File

@ -0,0 +1,21 @@
package com.github.nacabaro.vbhelper.battle
data class AdditionalInfo(
val avatar: String? = null,
val id: Long? = null,
val name: String? = null,
val status: String? = null
)
data class UserInfo(
val userId: String? = null,
val additionalInfo: AdditionalInfo? = null
)
data class AuthenticateResponse(
val success: Boolean,
val message: String? = null,
val userInfo: UserInfo? = null,
val sessionToken: String? = null
)

View File

@ -0,0 +1,16 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.github.nacabaro.vbhelper.source.AuthRepository
private const val BATTLE_AUTH_PREFERENCES_NAME = "battle_auth_preferences"
val Context.battleAuthStore: androidx.datastore.core.DataStore<Preferences> by preferencesDataStore(
name = BATTLE_AUTH_PREFERENCES_NAME
)
class BattleAuthContainer(private val context: Context) {
val authRepository: AuthRepository = AuthRepository(context.battleAuthStore)
}

View File

@ -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<String>
)
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<String, Bitmap>()
// Get the external storage directory for sprite files
private fun getSpriteBaseDir(): File {
val externalDir = android.os.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<String> {
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<String> {
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()
}
}
}

View File

@ -0,0 +1,167 @@
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>(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
// 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()
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
}
}

View File

@ -0,0 +1,120 @@
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<android.graphics.Bitmap?>(null) }
var animationProgress by remember { mutableStateOf(0f) }
var scale by remember { mutableStateOf(0.5f) }
var alpha by remember { mutableStateOf(1f) }
LaunchedEffect(isVisible) {
if (isVisible) {
// 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
)
}
}
}

View File

@ -0,0 +1,172 @@
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<String, Bitmap>()
// Get the external storage directory for hit effect sprites
private fun getHitSpritesDir(): File {
val externalDir = android.os.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
}
// 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<String> {
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<String> {
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()
}
}

View File

@ -0,0 +1,132 @@
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<String, Bitmap>()
// Get the external storage directory for sprite files
private fun getSpriteBaseDir(): File {
val externalDir = android.os.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
}
// 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<Int> {
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<String> {
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()
}
}

View File

@ -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<OpponentsDataModel>
}

View File

@ -0,0 +1,5 @@
package com.github.nacabaro.vbhelper.battle
data class OpponentsDataModel (
val opponentsList: ArrayList<APIBattleCharacter>
):java.io.Serializable

View File

@ -0,0 +1,16 @@
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,
val opponentCharaId: String? = null, // Server provides opponent's charaId from the match
val playerMaxHP: Int? = null, // Server should provide max HP for resumed matches
val opponentMaxHP: Int? = null // Server should provide max HP for resumed matches
):java.io.Serializable

View File

@ -0,0 +1,22 @@
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: Long,
@Query("playerDigi") playerDigi: String,
@Query("playerStage") playerStage: Int,
@Query("critBar") critBar: Int,
@Query("opponentDigi") opponentDigi: String,
@Query("opponentStage") opponentStage: Int,
@Query("action") action: String? = null // Optional: "quit" or "rejoin"
): Call<PVPDataModel>
}

View File

@ -0,0 +1,377 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import retrofit2.Retrofit
import android.widget.Toast
import retrofit2.*
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
class RetrofitHelper {
/**
* Creates an OkHttpClient with authentication interceptor for game endpoints.
* Requires a non-null, non-empty token.
*/
private fun createAuthenticatedClient(token: String): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(token))
.addInterceptor(loggingInterceptor)
.build()
}
/**
* Gets the session token from AuthRepository for API calls.
* Falls back to nacatech token if session token is not available (backward compatibility).
*/
private fun getAuthToken(context: Context): String? {
return try {
val authContainer = BattleAuthContainer(context)
runBlocking {
// Prefer session token, fall back to nacatech token for backward compatibility
val sessionToken = authContainer.authRepository.sessionToken.first()
if (!sessionToken.isNullOrEmpty()) {
println("RetrofitHelper: Using sessionToken for API call")
sessionToken
} else {
// Fallback to nacatech token (slower, but works)
val nacatechToken = authContainer.authRepository.authToken.first()
if (!nacatechToken.isNullOrEmpty()) {
println("RetrofitHelper: No sessionToken found, falling back to nacatechToken")
}
nacatechToken
}
}
} catch (e: Exception) {
println("RetrofitHelper: Error getting auth token: ${e.message}")
null
}
}
/**
* Creates a Retrofit instance with authentication for game endpoints.
*/
private fun createAuthenticatedRetrofit(context: Context): Retrofit? {
val token = getAuthToken(context)
if (token.isNullOrEmpty()) {
println("RetrofitHelper: No auth token available")
return null
}
val client = createAuthenticatedClient(token)
return Retrofit.Builder()
.baseUrl("http://battle.io-void.com:8080/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
/**
* Handles HTTP error responses (401, 403, 429).
* For 401/403, clears authentication state to trigger re-authentication.
*/
private fun handleErrorResponse(context: Context, response: Response<*>, errorMessage: String) {
when (response.code()) {
401 -> {
println("RetrofitHelper: Authentication failed (401) - token may be expired")
clearAuthAndNotify(context, "Authentication failed. Please log in again.")
}
403 -> {
println("RetrofitHelper: Access forbidden (403) - token may be expired or invalid")
// 403 could mean expired token, so clear auth state to trigger re-authentication
clearAuthAndNotify(context, "Session expired. Please log in again.")
}
429 -> {
println("RetrofitHelper: Rate limit exceeded (429)")
Toast.makeText(context, "Too many requests. Please wait a moment.", Toast.LENGTH_SHORT).show()
}
else -> {
println("RetrofitHelper: API error (${response.code()}): $errorMessage")
Toast.makeText(context, "Request failed: ${response.code()}", Toast.LENGTH_SHORT).show()
}
}
}
/**
* Clears authentication state and shows a message.
* This will trigger BattlesScreen to detect the auth state change and open the login page.
*/
private fun clearAuthAndNotify(context: Context, message: String) {
try {
val authContainer = BattleAuthContainer(context)
CoroutineScope(Dispatchers.IO).launch {
authContainer.authRepository.logout()
println("RetrofitHelper: Cleared authentication state due to expired/invalid token")
}
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
} catch (e: Exception) {
println("RetrofitHelper: Error clearing auth state: ${e.message}")
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) {
//println("RetrofitHelper: Starting API call for stage: $stage")
try {
// Create an authenticated Retrofit instance
val retrofit = createAuthenticatedRetrofit(context)
if (retrofit == null) {
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
return
}
// Create an ApiService instance from the Retrofit instance.
val service: OpponentService = retrofit.create<OpponentService>(OpponentService::class.java)
//println("RetrofitHelper: Service created")
// Call the getopponents() method of the ApiService
// to make an API request.
val call: Call<OpponentsDataModel> = 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<OpponentsDataModel> {
override fun onFailure(call: Call<OpponentsDataModel>, 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<OpponentsDataModel>, response: Response<OpponentsDataModel>) {
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 {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: Response not successful - Error: $errorBody")
handleErrorResponse(context, response, errorBody ?: "Unknown error")
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in getOpponents: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
/*
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://battle.io-void.com:8080/").addConverterFactory(
GsonConverterFactory.create()).build()
// Create an ApiService instance from the Retrofit instance.
val service: CombatService = retrofit.create<CombatService>(CombatService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<CombatDataModel> = service.getwinner(stage)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<CombatDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<CombatDataModel>, 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<CombatDataModel>, response: Response<CombatDataModel>) {
// 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://battle.io-void.com:8080/").addConverterFactory(
GsonConverterFactory.create()).build()
// Create an ApiService instance from the Retrofit instance.
val service: BattleService = retrofit.create<BattleService>(BattleService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<BattleDataModel> = service.getwinner(playerDigi, playerStage, opponentDigi, opponentStage)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<BattleDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<BattleDataModel>, 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<BattleDataModel>, response: Response<BattleDataModel>) {
// 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: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) {
getPVPWinner(context, apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, null, callback)
}
fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, action: String?, callback: (PVPDataModel) -> Unit) {
try {
// Create an authenticated Retrofit instance
val retrofit = createAuthenticatedRetrofit(context)
if (retrofit == null) {
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
return
}
// Create an ApiService instance from the Retrofit instance.
val service: PVPService = retrofit.create<PVPService>(PVPService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<PVPDataModel> = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, action)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<PVPDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<PVPDataModel>, t: Throwable) {
// This method is called when the API request fails.
println("RetrofitHelper: PVP API call failed: ${t.message}")
t.printStackTrace()
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<PVPDataModel>, response: Response<PVPDataModel>) {
// This method is called when the API response is received successfully.
println("RetrofitHelper: PVP API response received - Code: ${response.code()}")
if(response.isSuccessful){
// If the response is successful, parse the
// response body to a DataModel object.
val apiResults: PVPDataModel = response.body() as PVPDataModel
// Call the callback function with the DataModel
// object as a parameter.
callback(apiResults)
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: PVP API response not successful - Code: ${response.code()}, Error: $errorBody")
handleErrorResponse(context, response, errorBody ?: "Unknown error")
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in getPVPWinner: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) {
//println("RetrofitHelper: Starting validate API call with token: $token")
if (token.isEmpty()) {
println("RetrofitHelper: ERROR - Token is empty!")
Toast.makeText(context, "Authentication failed: Token is empty", Toast.LENGTH_SHORT).show()
return
}
try {
// Add logging interceptor to see the actual HTTP request
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://battle.io-void.com:8080/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service: AuthService = retrofit.create<AuthService>(AuthService::class.java)
val request = AuthenticateRequest(userToken = token)
// Use login endpoint instead of validate to get sessionToken
val call: Call<AuthenticateResponse> = service.login(request)
call.enqueue(object : Callback<AuthenticateResponse> {
override fun onFailure(call: Call<AuthenticateResponse>, t: Throwable) {
println("RetrofitHelper: Validate API call failed: ${t.message}")
t.printStackTrace()
Toast.makeText(context, "Authentication failed: ${t.message}", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<AuthenticateResponse>, response: Response<AuthenticateResponse>) {
if (response.isSuccessful) {
val authResponse: AuthenticateResponse? = response.body()
if (authResponse != null) {
callback(authResponse)
} else {
println("RetrofitHelper: Validation failed: Invalid response body")
Toast.makeText(context, "Authentication failed: Invalid response", Toast.LENGTH_SHORT).show()
}
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: Validate response not successful - Code: ${response.code()}, Error: $errorBody")
Toast.makeText(context, "Authentication failed: ${response.code()}", Toast.LENGTH_SHORT).show()
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in validate: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Authentication failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -0,0 +1,328 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.FileInputStream
class SpriteFileManager(private val context: Context) {
// Get the external storage directory where files are already located
fun getExternalSpriteBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites")
}
// Get the internal storage directory for sprite files
private fun getInternalSpriteBaseDir(): File {
return File(context.filesDir, "battle_sprites")
}
fun copySpriteFilesToInternalStorage() {
try {
println("Starting sprite file copy process from external storage to internal storage...")
val externalDir = getExternalSpriteBaseDir()
val internalDir = getInternalSpriteBaseDir()
// Check if external directory exists
if (!externalDir.exists()) {
println("External sprite directory does not exist: ${externalDir.absolutePath}")
return
}
println("External sprite directory exists: ${externalDir.absolutePath}")
println("Copying to internal storage: ${internalDir.absolutePath}")
// Create internal directory if it doesn't exist
if (!internalDir.exists()) {
val created = internalDir.mkdirs()
println("Created internal sprite directory: $created")
}
// Copy all subdirectories from external to internal storage
val externalFiles = externalDir.listFiles()
if (externalFiles != null) {
println("Found ${externalFiles.size} items in external directory")
externalFiles.forEach { item ->
val targetItem = File(internalDir, item.name)
if (item.isDirectory) {
println("Copying directory: ${item.name}")
copyDirectory(item, targetItem)
} else {
println("Copying file: ${item.name}")
copyFile(item, targetItem)
}
}
}
println("Sprite files copied successfully to internal storage: ${internalDir.absolutePath}")
} catch (e: Exception) {
println("Error copying sprite files to internal storage: ${e.message}")
e.printStackTrace()
}
}
fun copySpriteFilesToExternalStorage() {
try {
println("Starting sprite file copy process to external storage...")
// 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 = getExternalSpriteBaseDir()
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 copyDirectory(sourceDir: File, targetDir: File) {
if (!targetDir.exists()) {
targetDir.mkdirs()
}
val files = sourceDir.listFiles()
if (files != null) {
files.forEach { file ->
val targetFile = File(targetDir, file.name)
if (file.isDirectory) {
copyDirectory(file, targetFile)
} else {
copyFile(file, targetFile)
}
}
}
}
private fun copyFile(sourceFile: File, targetFile: File) {
try {
val inputStream = FileInputStream(sourceFile)
val outputStream = FileOutputStream(targetFile)
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
println("Copied: ${sourceFile.name} -> ${targetFile.absolutePath}")
} catch (e: IOException) {
println("Error copying file ${sourceFile.name}: ${e.message}")
}
}
private fun copyAssetFile(assetPath: String, targetFile: File) {
try {
val inputStream = context.assets.open(assetPath)
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 = getExternalSpriteBaseDir()
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 in external storage:")
println(" battle_sprites exists: $battleSpritesExist")
println(" extracted_assets exists: $assetsExist")
println(" extracted_digimon_stats exists: $statsExist")
println(" extracted_atksprites exists: $atkspritesExist")
println(" extracted_battlebgs exists: $battlebgsExist")
*/
return battleSpritesExist && assetsExist && statsExist && atkspritesExist && battlebgsExist
}
fun clearSpriteFiles() {
try {
val battleSpritesDir = getInternalSpriteBaseDir()
if (battleSpritesDir.exists()) {
deleteDirectory(battleSpritesDir)
println("Cleared battle_sprites directory from internal storage")
}
} 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()
}
}
}

View File

@ -0,0 +1,38 @@
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<android.graphics.Bitmap?>(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")
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Sprite: $characterId frame $frameNumber",
modifier = modifier,
contentScale = contentScale
)
}
}

View File

@ -0,0 +1,45 @@
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<Bitmap?>(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)
}
bitmap = loadedBitmap
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Attack Sprite",
modifier = modifier,
contentScale = contentScale
)
}
}

View File

@ -0,0 +1,66 @@
package com.github.nacabaro.vbhelper.source
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class AuthRepository(
private val dataStore: DataStore<Preferences>
) {
private companion object {
val IS_AUTHENTICATED = booleanPreferencesKey("is_authenticated")
val AUTH_TOKEN = stringPreferencesKey("auth_token") // Nacatech token (for re-authentication)
val SESSION_TOKEN = stringPreferencesKey("session_token") // Session token (for API calls)
val USER_ID = longPreferencesKey("user_id")
}
val isAuthenticated: Flow<Boolean> = dataStore.data
.map { preferences ->
preferences[IS_AUTHENTICATED] ?: false
}
val authToken: Flow<String?> = dataStore.data
.map { preferences ->
preferences[AUTH_TOKEN]
}
val sessionToken: Flow<String?> = dataStore.data
.map { preferences ->
preferences[SESSION_TOKEN]
}
val userId: Flow<Long?> = dataStore.data
.map { preferences ->
preferences[USER_ID]
}
suspend fun setAuthenticated(isAuthenticated: Boolean, nacatechToken: String? = null, sessionToken: String? = null, userId: Long? = null) {
dataStore.edit { preferences ->
preferences[IS_AUTHENTICATED] = isAuthenticated
if (nacatechToken != null) {
preferences[AUTH_TOKEN] = nacatechToken
}
if (sessionToken != null) {
preferences[SESSION_TOKEN] = sessionToken
}
if (userId != null) {
preferences[USER_ID] = userId
}
}
}
suspend fun logout() {
dataStore.edit { preferences ->
preferences[IS_AUTHENTICATED] = false
preferences.remove(AUTH_TOKEN)
preferences.remove(SESSION_TOKEN)
preferences.remove(USER_ID)
}
}
}