mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-09-30 23:21:36 +00:00
Compare commits
192
Commits
widget
...
99d309adc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99d309adc1 | ||
|
|
2760af5d2b | ||
|
|
931f730406 | ||
|
|
4eaf6cdf1b | ||
|
|
416b30f7a8 | ||
|
|
df713855f5 | ||
|
|
e5ccff16d0 | ||
|
|
4b866aad8a | ||
|
|
33ad2728b8 | ||
|
|
05f04fdc49 | ||
|
|
07315fe11b | ||
|
|
2e72b543f6 | ||
|
|
81bfec5ac5 | ||
|
|
8ec98b9d39 | ||
|
|
e549fb4469 | ||
|
|
8d62437aae | ||
|
|
7a8815694d | ||
|
|
464f2e48a7 | ||
|
|
f641e8aaa0 | ||
|
|
c67dc4bd02 | ||
|
|
8aa31e819b | ||
|
|
bd8883e5d1 | ||
|
|
b094d6ccd3 | ||
|
|
6c257b2080 | ||
|
|
04ea584c4f | ||
|
|
af27fc4933 | ||
|
|
1fffab25bd | ||
|
|
1a83527590 | ||
|
|
431be5faea | ||
|
|
9aa62f0d81 | ||
|
|
8fd4f6c414 | ||
|
|
f829efae68 | ||
|
|
895b704c8d | ||
|
|
d40428ccfd | ||
|
|
cd9a6d5d80 | ||
|
|
e0a086d9aa | ||
|
|
e787ea4a19 | ||
|
|
5273bae7b6 | ||
|
|
706f30ea49 | ||
|
|
3f29d725ea | ||
|
|
977896d296 | ||
|
|
654529c851 | ||
|
|
366f425539 | ||
|
|
37076a9bf0 | ||
|
|
8c99046edb | ||
|
|
c990a495b7 | ||
|
|
0e61723db1 | ||
|
|
8815907563 | ||
|
|
566e2ec977 | ||
|
|
65a7ccb221 | ||
|
|
2201b7d0fe | ||
|
|
a9c354ad8a | ||
|
|
9365bc0215 | ||
|
|
efa4bab144 | ||
|
|
6cce33c5f6 | ||
|
|
16fbfae0c2 | ||
|
|
0d174f1550 | ||
|
|
eb82c2afc1 | ||
|
|
c774fd1536 | ||
|
|
54b2905196 | ||
|
|
c4450296db | ||
|
|
d1908d629a | ||
|
|
67b56b3990 | ||
|
|
b0c5d6375d | ||
|
|
29ff2805c3 | ||
|
|
b4d509aad9 | ||
|
|
44c6382356 | ||
|
|
489e27b038 | ||
|
|
6be167bbed | ||
|
|
61dfc2ce0d | ||
|
|
dce186737d | ||
|
|
cf272e8030 | ||
|
|
ffa6958a89 | ||
|
|
1d9879fb50 | ||
|
|
3fa072ce1e | ||
|
|
69245be8dd | ||
|
|
0f1feb88b8 | ||
|
|
5ddb8f5da9 | ||
|
|
0875b114d5 | ||
|
|
61daad459b | ||
|
|
14e941031c | ||
|
|
952fd5a871 | ||
|
|
ac02205f76 | ||
|
|
68ad57b78f | ||
|
|
73c393df64 | ||
|
|
0b3ce486cb | ||
|
|
7160eb792a | ||
|
|
07983d9403 | ||
|
|
cb9fe8e716 | ||
|
|
540766eefb | ||
|
|
af2c852bac | ||
|
|
8dc5bbbdde | ||
|
|
aac05a3b1c | ||
|
|
b827fdccbe | ||
|
|
2586190e5c | ||
|
|
1a6753d2aa | ||
|
|
9c581a5ebd | ||
|
|
0add667ef8 | ||
|
|
c122b71b46 | ||
|
|
f8ec899335 | ||
|
|
b95edfa2a1 | ||
|
|
5ed7d117f5 | ||
|
|
16cd7abce8 | ||
|
|
6a6369ae9e | ||
|
|
1bbaf66c24 | ||
|
|
ba03be808e | ||
|
|
bb1c29bbb4 | ||
|
|
f0760f9ed0 | ||
|
|
61a2439f84 | ||
|
|
5bb9fe5209 | ||
|
|
26fc0ced56 | ||
|
|
371a850d45 | ||
|
|
d833a89c17 | ||
|
|
1a570170ca | ||
|
|
1566876106 | ||
|
|
019f07d827 | ||
|
|
4474e6689d | ||
|
|
28cb824bf3 | ||
|
|
0a643053af | ||
|
|
5989f48355 | ||
|
|
de3d312a32 | ||
|
|
cd33d06ecf | ||
|
|
6ea9946412 | ||
|
|
ec3058b511 | ||
|
|
26842d1b1b | ||
|
|
b74b04cda9 | ||
|
|
481c0b6d9a | ||
|
|
a3bebcf290 | ||
|
|
fb09004d13 | ||
|
|
dd1a62d184 | ||
|
|
f96f00e05a | ||
|
|
3c17e93d0f | ||
|
|
f8ce81e932 | ||
|
|
b3cf823c3f | ||
|
|
7843be7004 | ||
|
|
f0f1d9830e | ||
|
|
c404f4f436 | ||
|
|
71ba5e0207 | ||
|
|
fb09350825 | ||
|
|
615fb85204 | ||
|
|
7bb7693876 | ||
|
|
2b33042fe0 | ||
|
|
3687ff2c21 | ||
|
|
c973030d9d | ||
|
|
9f7e452850 | ||
|
|
37e5efa874 | ||
|
|
c5cebd8213 | ||
|
|
cfa52bce9b | ||
|
|
acd990d32b | ||
|
|
c8690152bc | ||
|
|
eff2fadb55 | ||
|
|
8f790eea41 | ||
|
|
9d0e68fb8a | ||
|
|
31fab9dba4 | ||
|
|
7af8e00e6f | ||
|
|
4e12962c05 | ||
|
|
b3a4ced28d | ||
|
|
d7b10b1ae8 | ||
|
|
942d843601 | ||
|
|
4d8dcde26e | ||
|
|
3b762d6195 | ||
|
|
023af17b23 | ||
|
|
2901bcf0da | ||
|
|
266658342a | ||
|
|
a4b159da45 | ||
|
|
fa8546f283 | ||
|
|
d86ee00109 | ||
|
|
6c9d057917 | ||
|
|
a044d24f5f | ||
|
|
1d21155198 | ||
|
|
09ee139add | ||
|
|
8d9c507645 | ||
|
|
c947e2519c | ||
|
|
9bcbe85b7f | ||
|
|
a72718273c | ||
|
|
bd0cc46398 | ||
|
|
a011ae39a4 | ||
|
|
0b1eed7da9 | ||
|
|
36c3e25fd3 | ||
|
|
ac05dfc541 | ||
|
|
220a61a553 | ||
|
|
d847f600f1 | ||
|
|
f7a4039d66 | ||
|
|
1eed6a4c17 | ||
|
|
1a09e40d1d | ||
|
|
13051145b7 | ||
|
|
1a150ce7ce | ||
|
|
39ba48d867 | ||
|
|
3f324ca141 | ||
|
|
1e28a8c249 | ||
|
|
9f5f806786 | ||
|
|
f7b3b7256a |
@@ -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
|
||||
@@ -1,38 +1,69 @@
|
||||
# VBHelper
|
||||
|
||||
## Developer Setup
|
||||
Application to interact with the Vital series, VB, VH, VBC and VBBE.
|
||||
|
||||
1. Clone vb-nfc-reader (https://github.com/cfogrady/lib-vb-nfc)
|
||||
2. Run vb-nfc-reader/publishToMavenLocal gradle task in the lib-vb-nfc project.
|
||||
3. Clone vb-dim-reader (https://github.com/cfogrady/vb-dim-reader)
|
||||
4. Run publishToMavenLocal gradle task in the vb-dim-reader project.
|
||||
5. Create res/values/keys.xml within the app module.
|
||||
6. Populate with:
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="password1">beHmacKey1</string>
|
||||
<string name="password2">beHmacKey2</string>
|
||||
<string name="decryptionKey">aesKey</string>
|
||||
<integer-array name="substitutionArray">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
<item>11</item>
|
||||
<item>12</item>
|
||||
<item>13</item>
|
||||
<item>14</item>
|
||||
<item>15</item>
|
||||
</integer-array>
|
||||
</resources>
|
||||
```
|
||||
7. Replace the values in the keys.xml file with those extracted from the original app.
|
||||
8. Run
|
||||
## Current state of the project
|
||||
|
||||
Right now the project is still under development, and until further notice, any database updates will result in having to erase application data.
|
||||
|
||||
This document will be updated once the application does not need any more database resets.
|
||||
|
||||
## Features
|
||||
|
||||
As of now, the project allows you to read characters, view characters stats, and send them back to your watch.
|
||||
|
||||
You can also apply items to the characters read, such as special missions, or change timers, and store characters in the storage section.
|
||||
|
||||
You also earn new items every time an item such as a special mission (VB only) or a character completes an in-app adventure mission.
|
||||
|
||||
App also comes with a dex that will update every time a new character is added, and allows you to see evolution requirements and current adventure stage in the watch.
|
||||
|
||||
## How to set up
|
||||
|
||||
1. Download the latest version for VB Arena APK from a trustworthy source. If your download is a standalone APK, continue to step 2. Otherwise, if your download is an XAPK, do the following:
|
||||
|
||||
1. Using your phone file manager, rename the XAPK file to ZIP, and extract its contents. You can also do this with any other device, such as Windows, macOS and Linux.
|
||||
|
||||
2. Once the files are extracted, look for an APK called `com.bandai.vitalbraceletarena.apk`. Copy it somewhere else, you will need it.
|
||||
|
||||
2. Install an APK release for VB Helper. You will find the releases [here](http://github.com/nacabaro/vbhelper/releases). Download the latest release and install its APK.
|
||||
|
||||
Note, in the current stage of the project, you will have to delete the old application from your device. If the app keeps crashing after installing, clear application data and storage.
|
||||
|
||||
3. Import secrets in the app. These secrets will allow the app to talk to the watch. On the main screen, click on the gear icon, then `Import secrets`.
|
||||
|
||||
You will be prompted to choose a file. Choose the APK file that was previously obtained.
|
||||
|
||||
4. Import cards. Due to copyright laws, we cannot offer the characters and sprites themselves in the application. In order to import the cards do the following.
|
||||
|
||||
1. Using your own DiM/BEm cards, dump the cards to your device. You can get an in-depth tutorial in [here](http://mrblinky.net/digimon/vb/dimcardtool/dimcardtool.html). You can download the dump tool from [here](http://mrblinky.net/digimon/vb/dimcardtool/)
|
||||
|
||||
2. Once installed the tool and drivers, open the tool, connect your DiM/BEm reader hardware to yout computer and click on Read card.
|
||||
|
||||
3. Transfer the resulting file to your mobile device. You can put them anywhere, as long as they are accessible. My recommendation is to put them under a folder called `Cards` in your `Internal storage` or `SD Card`
|
||||
|
||||
4. In the app, click on import card. Next choose the BIN file corresponding to the card you want to import.
|
||||
|
||||
**Note: if you do not import the card, whenever you attempt to read a character from th watch, the character you read will get deleted.**
|
||||
|
||||
5. App will now be ready to be used.
|
||||
|
||||
## Planned features
|
||||
|
||||
- Online battles, undegoing development by `lightheel`.
|
||||
|
||||
- VitalWear compatibility, undergoing development by `cfogrady`.
|
||||
|
||||
- Support for multiple languages, not yet started.
|
||||
|
||||
- Database backup/restore.
|
||||
|
||||
## Credits
|
||||
|
||||
- `cyanic` for helping us understand more about the VB connection protocol.
|
||||
|
||||
- `cfogrady` for making both [`VB-DIM-Reader`](https://github.com/cfogrady/VB-DIM-Reader) and [`lib-vb-nfc`](https://github.com/cfogrady/lib-vb-nfc)
|
||||
|
||||
- `lightheel` for working on the online component in the application, both server and battle client.
|
||||
|
||||
- `shvstrz` for the app icon.
|
||||
|
||||
+41
-21
@@ -1,21 +1,22 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
|
||||
id("com.google.protobuf")
|
||||
alias(libs.plugins.vksp)
|
||||
alias(libs.plugins.vprotobuf)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.github.nacabaro.vbhelper"
|
||||
compileSdk = 35
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.github.nacabaro.vbhelper"
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
targetSdk = 37
|
||||
versionCode = 1
|
||||
versionName = "Alpha 0.1"
|
||||
versionName = "Alpha 0.6.5"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -33,9 +34,7 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
@@ -45,14 +44,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_11
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.27.0"
|
||||
}
|
||||
|
||||
// Generates the java Protobuf-lite code for the Protobufs in this project. See
|
||||
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
|
||||
// for more information.
|
||||
generateProtoTasks {
|
||||
all().forEach { task ->
|
||||
task.builtins {
|
||||
@@ -68,10 +70,13 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.vb.nfc.reader)
|
||||
implementation(libs.dim.reader)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
annotationProcessor(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.material)
|
||||
implementation(libs.protobuf.javalite)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -80,15 +85,30 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.play.services.wearable)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.tinylog.api)
|
||||
implementation(libs.tinylog.impl)
|
||||
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
|
||||
ksp(libs.androidx.room.compiler)
|
||||
annotationProcessor(libs.androidx.room.compiler)
|
||||
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
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(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.gson)
|
||||
|
||||
// HTTP request logging
|
||||
implementation(libs.okhttp.logging.interceptor)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,12 @@
|
||||
|
||||
<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.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application
|
||||
android:name=".di.VBHelper"
|
||||
@@ -15,17 +21,46 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.VBHelper"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.VBHelper">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<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" />
|
||||
<data android:host="localhost" android:port="8080" />
|
||||
<data android:host="127.0.0.1" android:port="8080" />
|
||||
<data android:pathPrefix="/authenticate" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/x-vitalwear-character" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,22 @@
|
||||
package com.github.cfogrady.vitalwear.common.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
|
||||
object SharedDatabaseFactory {
|
||||
private const val DATABASE_NAME = "shared_transfer.db"
|
||||
|
||||
@Volatile
|
||||
private var instance: SharedTransferDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): SharedTransferDatabase {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
SharedTransferDatabase::class.java,
|
||||
DATABASE_NAME
|
||||
).build().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.cfogrady.vitalwear.common.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
SharedTransferSeenEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class SharedTransferDatabase : RoomDatabase() {
|
||||
abstract fun transferSeenDao(): SharedTransferSeenDao
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.github.cfogrady.vitalwear.common.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Dao
|
||||
interface SharedTransferSeenDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insert(entry: SharedTransferSeenEntity): Long
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE ${SharedTransferSeenEntity.TABLE}
|
||||
SET cardName = :cardName,
|
||||
seenAtEpochMillis = :seenAtEpochMillis
|
||||
WHERE cardLookupKey = :cardLookupKey
|
||||
AND slotId = :slotId
|
||||
"""
|
||||
)
|
||||
fun update(cardName: String, cardLookupKey: String, slotId: Int, seenAtEpochMillis: Long)
|
||||
|
||||
@Transaction
|
||||
fun markSeen(cardName: String, slotId: Int, seenAtEpochMillis: Long) {
|
||||
if (slotId < 0) {
|
||||
return
|
||||
}
|
||||
val cardLookupKey = cardName.lowercase().filter { it.isLetterOrDigit() }
|
||||
if (cardLookupKey.isBlank()) {
|
||||
return
|
||||
}
|
||||
val inserted = insert(
|
||||
SharedTransferSeenEntity(
|
||||
cardName = cardName,
|
||||
cardLookupKey = cardLookupKey,
|
||||
slotId = slotId,
|
||||
seenAtEpochMillis = seenAtEpochMillis,
|
||||
)
|
||||
)
|
||||
if (inserted == -1L) {
|
||||
update(cardName, cardLookupKey, slotId, seenAtEpochMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.github.cfogrady.vitalwear.common.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = SharedTransferSeenEntity.TABLE,
|
||||
indices = [
|
||||
Index(value = ["cardLookupKey", "slotId"], unique = true)
|
||||
]
|
||||
)
|
||||
data class SharedTransferSeenEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val cardName: String,
|
||||
val cardLookupKey: String,
|
||||
val slotId: Int,
|
||||
val seenAtEpochMillis: Long,
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE = "TransferSeen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.github.nacabaro.vbhelper
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.cfogrady.vitalwear.protos.Character
|
||||
import com.github.nacabaro.vbhelper.navigation.AppNavigation
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
|
||||
@@ -14,12 +19,18 @@ import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImp
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.cardScreen.CardScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter
|
||||
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
|
||||
private var initialRoute: String? = null
|
||||
|
||||
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
|
||||
if( onActivityLifecycleListeners[key] != null) {
|
||||
@@ -38,18 +49,23 @@ class MainActivity : ComponentActivity() {
|
||||
application.container.dataStoreSecretsRepository.secretsFlow,
|
||||
this,
|
||||
this::registerActivityLifecycleListener,
|
||||
this::unregisterActivityLifecycleListener
|
||||
this::unregisterActivityLifecycleListener,
|
||||
application.container.db,
|
||||
)
|
||||
val settingsScreenController = SettingsScreenControllerImpl(this)
|
||||
val itemsScreenController = ItemsScreenControllerImpl(this)
|
||||
val adventureScreenController = AdventureScreenControllerImpl(this)
|
||||
val storageScreenController = StorageScreenControllerImpl(this)
|
||||
val homeScreenController = HomeScreenControllerImpl(this)
|
||||
val spriteViewerController = SpriteViewerControllerImpl(this)
|
||||
val cardScreenController = CardScreenControllerImpl(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
initialRoute = getInitialRouteFromIntent(intent)
|
||||
|
||||
setContent {
|
||||
VBHelperTheme {
|
||||
MainApplication(
|
||||
@@ -58,12 +74,16 @@ class MainActivity : ComponentActivity() {
|
||||
itemsScreenController = itemsScreenController,
|
||||
adventureScreenController = adventureScreenController,
|
||||
homeScreenController = homeScreenController,
|
||||
storageScreenController = storageScreenController
|
||||
storageScreenController = storageScreenController,
|
||||
spriteViewerController = spriteViewerController,
|
||||
cardScreenController = cardScreenController,
|
||||
initialRoute = initialRoute
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("MainActivity", "Activity onCreated")
|
||||
handleImportIntent(intent)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -82,6 +102,71 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
initialRoute = getInitialRouteFromIntent(intent)
|
||||
// Optionally, you may want to trigger navigation here if needed
|
||||
handleImportIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleImportIntent(intent: Intent?) {
|
||||
val importUri = extractVitalWearImportUri(intent) ?: return
|
||||
val application = applicationContext as VBHelper
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = runCatching {
|
||||
contentResolver.openInputStream(importUri)?.use { inputStream ->
|
||||
val character = Character.parseFrom(inputStream)
|
||||
VitalWearCharacterImporter(application.container.db).importCharacter(character)
|
||||
} ?: VitalWearCharacterImporter.ImportResult(
|
||||
success = false,
|
||||
message = "VitalWear import file could not be opened."
|
||||
)
|
||||
}.getOrElse {
|
||||
VitalWearCharacterImporter.ImportResult(
|
||||
success = false,
|
||||
message = "VitalWear import failed: ${it.message ?: "Unknown error"}"
|
||||
)
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@MainActivity, result.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractVitalWearImportUri(intent: Intent?): Uri? {
|
||||
if (intent == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val isVitalWearImport = intent.type == VITALWEAR_CHARACTER_MIME
|
||||
if (!isVitalWearImport) {
|
||||
return null
|
||||
}
|
||||
|
||||
return when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
Intent.ACTION_VIEW -> intent.data
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialRouteFromIntent(intent: Intent?): String? {
|
||||
if (intent == null) return null
|
||||
val data = intent.data
|
||||
if (intent.action == Intent.ACTION_VIEW && data != null) {
|
||||
if (data.scheme == "vbhelper" && data.host == "auth") {
|
||||
return "Battle"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainApplication(
|
||||
scanScreenController: ScanScreenControllerImpl,
|
||||
@@ -90,7 +175,9 @@ class MainActivity : ComponentActivity() {
|
||||
adventureScreenController: AdventureScreenControllerImpl,
|
||||
storageScreenController: StorageScreenControllerImpl,
|
||||
homeScreenController: HomeScreenControllerImpl,
|
||||
|
||||
spriteViewerController: SpriteViewerControllerImpl,
|
||||
cardScreenController: CardScreenControllerImpl,
|
||||
initialRoute: String? = null
|
||||
) {
|
||||
AppNavigation(
|
||||
applicationNavigationHandlers = AppNavigationHandlers(
|
||||
@@ -99,8 +186,15 @@ class MainActivity : ComponentActivity() {
|
||||
itemsScreenController,
|
||||
adventureScreenController,
|
||||
storageScreenController,
|
||||
homeScreenController
|
||||
)
|
||||
homeScreenController,
|
||||
spriteViewerController,
|
||||
cardScreenController
|
||||
),
|
||||
initialRoute = initialRoute
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.nacabaro.vbhelper.battle
|
||||
|
||||
data class AuthenticateRequest(
|
||||
val userToken: String
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.nacabaro.vbhelper.battle
|
||||
|
||||
data class OpponentsDataModel (
|
||||
val opponentsList: ArrayList<APIBattleCharacter>
|
||||
):java.io.Serializable
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.github.nacabaro.vbhelper.companion.card
|
||||
|
||||
enum class Endian {
|
||||
Big,
|
||||
Little,
|
||||
}
|
||||
|
||||
fun ByteArray.getUInt32(index: Int = 0, endian: Endian = Endian.Little): UInt {
|
||||
require(this.size >= index + 4) { "Must be 4 bytes from index to get a UInt" }
|
||||
var result: UInt = 0u
|
||||
for (i in 0 until 4) {
|
||||
result = if (endian == Endian.Big) {
|
||||
result or ((this[index + i].toUInt() and 0xFFu) shl (8 * (3 - i)))
|
||||
} else {
|
||||
result or ((this[index + i].toUInt() and 0xFFu) shl (8 * i))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun ByteArray.getUInt16(index: Int = 0, endian: Endian = Endian.Little): UShort {
|
||||
require(this.size >= index + 2) { "Must be 2 bytes from index to get a UInt" }
|
||||
var result: UInt = 0u
|
||||
for (i in 0 until 2) {
|
||||
result = if (endian == Endian.Big) {
|
||||
result or ((this[index + i].toUInt() and 0xFFu) shl (8 * (1 - i)))
|
||||
} else {
|
||||
result or ((this[index + i].toUInt() and 0xFFu) shl (8 * i))
|
||||
}
|
||||
}
|
||||
return result.toUShort()
|
||||
}
|
||||
|
||||
fun UShort.toByteArray(endian: Endian = Endian.Little): ByteArray {
|
||||
val byteArray = byteArrayOf(0, 0)
|
||||
val asUInt = this.toUInt()
|
||||
for (i in 0 until 2) {
|
||||
if (endian == Endian.Little) {
|
||||
byteArray[i] = ((asUInt shr (8 * i)) and 255u).toByte()
|
||||
} else {
|
||||
byteArray[1 - i] = ((asUInt shr (8 * i)) and 255u).toByte()
|
||||
}
|
||||
}
|
||||
return byteArray
|
||||
}
|
||||
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
package com.github.nacabaro.vbhelper.companion.card
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.cfogrady.vb.dim.adventure.AdventureLevels
|
||||
import com.github.cfogrady.vb.dim.adventure.AdventureLevels.AdventureLevel
|
||||
import com.github.cfogrady.vb.dim.card.Card
|
||||
import com.github.cfogrady.vb.dim.card.DimReader
|
||||
import com.github.cfogrady.vb.dim.card.DimWriter
|
||||
import com.github.cfogrady.vb.dim.character.CharacterStats
|
||||
import com.github.cfogrady.vb.dim.character.CharacterStats.CharacterStatsEntry
|
||||
import com.github.cfogrady.vb.dim.fusion.AttributeFusions
|
||||
import com.github.cfogrady.vb.dim.fusion.SpecificFusions
|
||||
import com.github.cfogrady.vb.dim.fusion.SpecificFusions.SpecificFusionEntry
|
||||
import com.github.cfogrady.vb.dim.header.DimHeader
|
||||
import com.github.cfogrady.vb.dim.transformation.TransformationRequirements
|
||||
import com.github.cfogrady.vb.dim.transformation.TransformationRequirements.TransformationRequirementsEntry
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.companion.card.CompanionValidateCardActivity
|
||||
import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
|
||||
import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class CompanionImportCardActivity : ComponentActivity() {
|
||||
enum class ImportState {
|
||||
PickFile,
|
||||
NameOrUnique,
|
||||
LoadFile,
|
||||
UnlockCard,
|
||||
ImportCard,
|
||||
Success,
|
||||
}
|
||||
|
||||
private lateinit var filePickLauncher: ActivityResultLauncher<Array<String>>
|
||||
private lateinit var validationLauncher: ActivityResultLauncher<Intent>
|
||||
private var uri: Uri? = null
|
||||
private val importState = MutableStateFlow(ImportState.PickFile)
|
||||
private val cardName = MutableStateFlow("")
|
||||
private val uniqueSprites = MutableStateFlow(false)
|
||||
private val convertToBem = MutableStateFlow(false)
|
||||
private val importPercent = MutableStateFlow(0)
|
||||
private val importStatus = MutableStateFlow("Preparing transfer")
|
||||
private lateinit var card: Card<out DimHeader, out CharacterStats<out CharacterStatsEntry>, out TransformationRequirements<out TransformationRequirementsEntry>, out AdventureLevels<out AdventureLevel>, out AttributeFusions, out SpecificFusions<out SpecificFusionEntry>>
|
||||
|
||||
private val validatedCardManager by lazy {
|
||||
(application as VBHelper).validatedCardManager
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
filePickLauncher = buildFilePickLauncher()
|
||||
validationLauncher = buildValidationLauncher()
|
||||
setContent {
|
||||
val state by importState.collectAsState()
|
||||
when (state) {
|
||||
ImportState.PickFile -> {
|
||||
LaunchedEffect(true) {
|
||||
filePickLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
CompanionLoading(loadingText = stringResource(R.string.settings_companion_import_card_image_title))
|
||||
}
|
||||
ImportState.NameOrUnique -> NameOrUnique()
|
||||
ImportState.LoadFile -> {
|
||||
CompanionLoading(loadingText = "Loading Card Image")
|
||||
LaunchedEffect(true) {
|
||||
runCatching {
|
||||
loadCard()
|
||||
}.onFailure { error ->
|
||||
Timber.e(error, "Failed to load card image")
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
"Card load failed: ${error.message ?: "unknown error"}",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
importState.value = ImportState.NameOrUnique
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportState.UnlockCard -> {
|
||||
CompanionLoading(loadingText = stringResource(R.string.companion_validation_connect_vb))
|
||||
LaunchedEffect(true) {
|
||||
validationLauncher.launch(
|
||||
Intent(applicationContext, CompanionValidateCardActivity::class.java).apply {
|
||||
putExtra(CompanionValidateCardActivity.CARD_VALIDATED_KEY, card.header.dimId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
ImportState.ImportCard -> {
|
||||
val percent by importPercent.collectAsState()
|
||||
val status by importStatus.collectAsState()
|
||||
CompanionLoading(loadingText = "$status ($percent%)")
|
||||
LaunchedEffect(true) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val transferred = importCard()
|
||||
importState.value = if (transferred) ImportState.Success else ImportState.NameOrUnique
|
||||
} catch (error: Exception) {
|
||||
Timber.e(error, "Card transfer failed")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
"Card transfer failed: ${error.message ?: "unknown error"}",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
importState.value = ImportState.NameOrUnique
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportState.Success -> {
|
||||
LaunchedEffect(true) {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 1000)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = stringResource(R.string.companion_card_import_success))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NameOrUnique() {
|
||||
val selectedUri = uri
|
||||
val filePath = selectedUri?.path ?: "No file selected"
|
||||
val name by cardName.collectAsState()
|
||||
val unique by uniqueSprites.collectAsState()
|
||||
val convert by convertToBem.collectAsState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Button(modifier = Modifier.padding(end = 10.dp), onClick = { importState.value = ImportState.PickFile }) {
|
||||
Text(text = "File")
|
||||
}
|
||||
Text(text = filePath)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Name:", modifier = Modifier.padding(end = 10.dp))
|
||||
TextField(value = name, onValueChange = { cardName.value = it })
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Unique Sprites:")
|
||||
Checkbox(checked = unique, onCheckedChange = { uniqueSprites.value = it })
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Convert to BeM:")
|
||||
Checkbox(checked = convert, onCheckedChange = { convertToBem.value = it })
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
enabled = selectedUri != null,
|
||||
onClick = { importState.value = ImportState.LoadFile },
|
||||
) {
|
||||
Text(text = "Import")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFilePickLauncher(): ActivityResultLauncher<Array<String>> {
|
||||
return registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
if (it == null) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Card file selection cancelled.",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
importState.value = ImportState.NameOrUnique
|
||||
} else {
|
||||
uri = it
|
||||
val path = it.path ?: ""
|
||||
var name = path.substringAfterLast("/")
|
||||
if (name.contains('.')) {
|
||||
name = name.substringBeforeLast('.')
|
||||
}
|
||||
cardName.value = name
|
||||
importState.value = ImportState.NameOrUnique
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildValidationLauncher(): ActivityResultLauncher<Intent> {
|
||||
return registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val validatedCardId = result.data?.extras?.getInt(CompanionValidateCardActivity.CARD_VALIDATED_KEY, -1)
|
||||
if (result.resultCode != RESULT_OK || validatedCardId == null || validatedCardId == -1) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Card validation was cancelled.",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
importState.value = ImportState.NameOrUnique
|
||||
} else {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
validatedCardManager.addValidatedCard(validatedCardId)
|
||||
importState.value = ImportState.ImportCard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadCard() {
|
||||
val selectedUri = uri
|
||||
if (selectedUri == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
"No card file selected.",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
importState.value = ImportState.NameOrUnique
|
||||
return
|
||||
}
|
||||
val dimReader = DimReader()
|
||||
val inputStream = contentResolver.openInputStream(selectedUri)
|
||||
if (inputStream == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
"Unable to open selected card file.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
importState.value = ImportState.NameOrUnique
|
||||
return
|
||||
}
|
||||
inputStream.use {
|
||||
card = dimReader.readCard(it, false)
|
||||
if (validatedCardManager.isValidatedCard(card.header.dimId)) {
|
||||
importState.value = ImportState.ImportCard
|
||||
} else {
|
||||
importState.value = ImportState.UnlockCard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importCard(): Boolean {
|
||||
importPercent.value = 0
|
||||
importStatus.value = "Preparing card"
|
||||
val channelClient = Wearable.getChannelClient(this)
|
||||
val nodes = Wearable.getNodeClient(this).connectedNodes.await()
|
||||
Timber.i("Card transfer start: connectedNodes=${nodes.size}, ids=${nodes.joinToString { it.id }}")
|
||||
if (nodes.isEmpty()) {
|
||||
importStatus.value = getString(R.string.companion_firmware_no_watch)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
getString(R.string.companion_firmware_no_watch),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val cardWriter = DimWriter()
|
||||
val cardPayload = ByteArrayOutputStream().use { outputStream ->
|
||||
cardWriter.writeCard(card, outputStream)
|
||||
outputStream.toByteArray()
|
||||
}
|
||||
val cardNameBytes = cardName.value.toByteArray(Charset.defaultCharset())
|
||||
val metadataLength = cardNameBytes.size + 1 + 1 + 1 + 4
|
||||
val totalBytes = (metadataLength + cardPayload.size).toLong() * nodes.size
|
||||
var transferredBytes = 0L
|
||||
var successfulTransfers = 0
|
||||
val failedNodes = mutableListOf<String>()
|
||||
for (node in nodes) {
|
||||
val nodeName = node.displayName.takeIf { it.isNotBlank() } ?: node.id
|
||||
importStatus.value = "Connecting to $nodeName"
|
||||
try {
|
||||
Timber.i("Opening card channel for nodeId=${node.id}, displayName=${node.displayName}")
|
||||
val channel = channelClient.openChannel(node.id, ChannelTypes.CARD_DATA).await()
|
||||
channelClient.getOutputStream(channel).await().use { os ->
|
||||
val output = DataOutputStream(os)
|
||||
output.write(cardNameBytes)
|
||||
output.writeByte(0)
|
||||
output.writeByte(if (uniqueSprites.value) 1 else 0)
|
||||
output.writeByte(if (convertToBem.value) 1 else 0)
|
||||
output.writeInt(cardPayload.size)
|
||||
transferredBytes += metadataLength
|
||||
val metadataPercent = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
|
||||
importStatus.value = "Sending card to $nodeName"
|
||||
importPercent.value = metadataPercent
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
cardPayload.inputStream().use { payloadInput ->
|
||||
while (payloadInput.read(buffer).also { bytesRead = it } >= 0) {
|
||||
output.write(buffer, 0, bytesRead)
|
||||
transferredBytes += bytesRead
|
||||
val transferPercent = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
|
||||
importPercent.value = transferPercent
|
||||
}
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
channelClient.close(channel).await()
|
||||
successfulTransfers++
|
||||
Timber.i("Card transfer complete for nodeId=${node.id}, card=${cardName.value}, payloadBytes=${cardPayload.size}")
|
||||
} catch (error: Exception) {
|
||||
failedNodes.add("$nodeName: ${error.message ?: "unknown error"}")
|
||||
Timber.e(error, "Card transfer failed for nodeId=${node.id}, displayName=${node.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulTransfers == 0) {
|
||||
importStatus.value = "Transfer failed"
|
||||
importPercent.value = 0
|
||||
withContext(Dispatchers.Main) {
|
||||
val reason = failedNodes.firstOrNull() ?: "No watch accepted the transfer"
|
||||
Toast.makeText(this@CompanionImportCardActivity, "Card transfer failed: $reason", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
importStatus.value = "Transfer complete"
|
||||
importPercent.value = 100
|
||||
if (failedNodes.isNotEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CompanionImportCardActivity,
|
||||
"Sent to $successfulTransfers watch(es); ${failedNodes.size} failed.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
Timber.i("Companion-style card import sent to watch for ${cardName.value}; success=$successfulTransfers failures=${failedNodes.size}")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.github.nacabaro.vbhelper.companion.card
|
||||
|
||||
import android.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.MifareUltralight
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import timber.log.Timber
|
||||
|
||||
class CompanionValidateCardActivity : ComponentActivity(), NfcAdapter.ReaderCallback {
|
||||
companion object {
|
||||
const val CARD_VALIDATED_KEY = "CARD_VALIDATED"
|
||||
}
|
||||
|
||||
enum class CardValidationState {
|
||||
WaitingForVBConnect,
|
||||
ValidateCardOnVB,
|
||||
Success,
|
||||
}
|
||||
|
||||
private lateinit var nfcAdapter: NfcAdapter
|
||||
private val validationStateFlow = MutableStateFlow(CardValidationState.WaitingForVBConnect)
|
||||
private var cardIdToValidate: UShort = 0u
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
val inputCardId = intent.getIntExtra(CARD_VALIDATED_KEY, -1)
|
||||
if (inputCardId == -1) {
|
||||
Timber.e("CompanionValidateCardActivity called without card number!")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
cardIdToValidate = inputCardId.toUShort()
|
||||
if (maybeNfcAdapter == null) {
|
||||
Toast.makeText(this, getString(R.string.scan_no_nfc_on_device), Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
nfcAdapter = maybeNfcAdapter
|
||||
setContent {
|
||||
DisplayContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayContent() {
|
||||
val validationState by validationStateFlow.collectAsState()
|
||||
when (validationState) {
|
||||
CardValidationState.WaitingForVBConnect -> {
|
||||
CompanionLoading(loadingText = stringResource(R.string.companion_validation_connect_vb))
|
||||
}
|
||||
CardValidationState.ValidateCardOnVB -> {
|
||||
CompanionLoading(loadingText = stringResource(R.string.companion_validation_insert_card))
|
||||
}
|
||||
CardValidationState.Success -> {
|
||||
LaunchedEffect(true) {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val intent = Intent().apply {
|
||||
putExtra(CARD_VALIDATED_KEY, cardIdToValidate.toInt())
|
||||
}
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}, 1000)
|
||||
}
|
||||
Text(text = stringResource(R.string.companion_validation_success))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!nfcAdapter.isEnabled) {
|
||||
showWirelessSettings()
|
||||
} else {
|
||||
nfcAdapter.enableReaderMode(this, this, NfcAdapter.FLAG_READER_NFC_A, Bundle())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (this::nfcAdapter.isInitialized) {
|
||||
nfcAdapter.disableReaderMode(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showWirelessSettings() {
|
||||
Toast.makeText(this, getString(R.string.scan_nfc_must_be_enabled), Toast.LENGTH_SHORT).show()
|
||||
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||
}
|
||||
|
||||
override fun onTagDiscovered(tag: Tag?) {
|
||||
if (tag == null) {
|
||||
Timber.w("Tag discovery returned null tag")
|
||||
return
|
||||
}
|
||||
when (validationStateFlow.value) {
|
||||
CardValidationState.WaitingForVBConnect -> {
|
||||
val nfcData = MifareUltralight.get(tag)
|
||||
if (nfcData == null) {
|
||||
Timber.w("Unsupported NFC tech for validation")
|
||||
return
|
||||
}
|
||||
val writeSuccessful = runCatching {
|
||||
nfcData.connect()
|
||||
nfcData.use {
|
||||
val vbData = VBNfcData(nfcData)
|
||||
vbData.writeCardCheck(nfcData, cardIdToValidate)
|
||||
}
|
||||
true
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed during first validation NFC step")
|
||||
runOnUiThread {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Failed to validate card on bracelet. Please retry.",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
if (writeSuccessful) {
|
||||
validationStateFlow.value = CardValidationState.ValidateCardOnVB
|
||||
}
|
||||
}
|
||||
CardValidationState.ValidateCardOnVB -> {
|
||||
val nfcData = MifareUltralight.get(tag)
|
||||
if (nfcData == null) {
|
||||
Timber.w("Unsupported NFC tech for validation")
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
nfcData.connect()
|
||||
nfcData.use {
|
||||
val vbData = VBNfcData(nfcData)
|
||||
if (vbData.wasCardIdValidated(cardIdToValidate)) {
|
||||
validationStateFlow.value = CardValidationState.Success
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed during second validation NFC step")
|
||||
}
|
||||
}
|
||||
else -> Timber.w("Incorrect state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.github.nacabaro.vbhelper.companion.card
|
||||
|
||||
import android.nfc.tech.MifareUltralight
|
||||
import kotlin.experimental.and
|
||||
import kotlin.experimental.or
|
||||
|
||||
class VBNfcData(nfcData: MifareUltralight) {
|
||||
companion object {
|
||||
private const val ITEM_ID_BE: UShort = 4u
|
||||
private const val STATUS_READY_FLAG: Byte = 0b00000001
|
||||
private const val STATUS_DIM_READY_FLAG: Byte = 0b00000010
|
||||
private val STATUS_DIM_IS_READY = STATUS_READY_FLAG or STATUS_DIM_READY_FLAG
|
||||
private const val OPERATION_READY: Byte = 1
|
||||
private const val OPERATION_CHECK_DIM: Byte = 3
|
||||
}
|
||||
|
||||
val itemId: UShort
|
||||
val status: Byte
|
||||
val operation: Byte
|
||||
val dimId: UShort
|
||||
|
||||
init {
|
||||
val readData = nfcData.transceive(byteArrayOf(0x30, 0x04))
|
||||
itemId = readData.getUInt16(4, Endian.Big)
|
||||
status = readData[8]
|
||||
if (itemId == ITEM_ID_BE) {
|
||||
operation = readData[9]
|
||||
dimId = readData.getUInt16(10, Endian.Big)
|
||||
} else {
|
||||
dimId = readData[9].toUShort()
|
||||
operation = readData[10]
|
||||
}
|
||||
}
|
||||
|
||||
fun writeCardCheck(nfcData: MifareUltralight, dimId: UShort) {
|
||||
if (itemId == ITEM_ID_BE) {
|
||||
val dimData = dimId.toByteArray(endian = Endian.Big)
|
||||
nfcData.writePage(6, byteArrayOf(STATUS_READY_FLAG, OPERATION_CHECK_DIM, dimData[0], dimData[1]))
|
||||
} else {
|
||||
nfcData.writePage(6, byteArrayOf(STATUS_READY_FLAG, dimId.toByte(), OPERATION_CHECK_DIM, 0))
|
||||
}
|
||||
}
|
||||
|
||||
fun wasCardIdValidated(dimId: UShort): Boolean {
|
||||
return dimId == this.dimId && operation == OPERATION_READY && (status and STATUS_DIM_IS_READY == STATUS_DIM_IS_READY)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.github.nacabaro.vbhelper.companion.common
|
||||
|
||||
object ChannelTypes {
|
||||
const val CARD_DATA = "com.github.cfogrady.vitalwear/CARD_DATA"
|
||||
const val FIRMWARE_DATA = "com.github.cfogrady.vitalwear/FIRMWARE_DATA"
|
||||
const val CHARACTER_DATA = "com.github.cfogrady.vitalwear/CHARACTER_DATA"
|
||||
const val SEND_LOGS_REQUEST = "SEND_LOGS_REQUEST"
|
||||
const val LOGS_DATA = "LOG_DATA"
|
||||
}
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.github.nacabaro.vbhelper.companion.firmware
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
|
||||
import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.DataOutputStream
|
||||
|
||||
class CompanionFirmwareImportActivity : ComponentActivity() {
|
||||
enum class FirmwareImportState {
|
||||
PickFirmware,
|
||||
LoadFirmware,
|
||||
}
|
||||
|
||||
private val importState = MutableStateFlow(FirmwareImportState.PickFirmware)
|
||||
private val transferPercent = MutableStateFlow(0)
|
||||
private val transferStatus = MutableStateFlow("Preparing transfer")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val firmwareImportActivity = buildFirmwareFilePickLauncher()
|
||||
setContent {
|
||||
BuildScreen(firmwareImportActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BuildScreen(firmwareImportActivity: ActivityResultLauncher<Array<String>>) {
|
||||
val state by importState.collectAsState()
|
||||
val percent by transferPercent.collectAsState()
|
||||
val status by transferStatus.collectAsState()
|
||||
if (state == FirmwareImportState.PickFirmware) {
|
||||
LaunchedEffect(true) {
|
||||
firmwareImportActivity.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
val loadingText = if (state == FirmwareImportState.PickFirmware) {
|
||||
getString(R.string.companion_loading_select_firmware)
|
||||
} else {
|
||||
"$status ($percent%)"
|
||||
}
|
||||
CompanionLoading(loadingText = loadingText)
|
||||
}
|
||||
|
||||
private fun buildFirmwareFilePickLauncher(): ActivityResultLauncher<Array<String>> {
|
||||
return registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
importState.value = FirmwareImportState.LoadFirmware
|
||||
if (it == null) {
|
||||
finish()
|
||||
} else {
|
||||
Timber.i("Path: ${it.path}")
|
||||
importFirmware(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun importFirmware(uri: Uri) {
|
||||
transferPercent.value = 0
|
||||
transferStatus.value = "Reading firmware file"
|
||||
val channelClient = Wearable.getChannelClient(this)
|
||||
val nodeListTask = Wearable.getNodeClient(this).connectedNodes
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val nodes = nodeListTask.await()
|
||||
if (nodes.isEmpty()) {
|
||||
transferStatus.value = getString(R.string.companion_firmware_no_watch)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@CompanionFirmwareImportActivity, getString(R.string.companion_firmware_no_watch), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
importState.value = FirmwareImportState.PickFirmware
|
||||
return@launch
|
||||
}
|
||||
|
||||
val firmware = contentResolver.openInputStream(uri)?.use { input ->
|
||||
input.readBytes()
|
||||
} ?: throw IllegalStateException("Unable to read selected firmware file")
|
||||
|
||||
transferPercent.value = 5
|
||||
transferStatus.value = "Sending firmware"
|
||||
val totalBytes = (firmware.size + 4).toLong() * nodes.size
|
||||
var transferredBytes = 0L
|
||||
for (node in nodes) {
|
||||
val channel = channelClient.openChannel(node.id, ChannelTypes.FIRMWARE_DATA).await()
|
||||
try {
|
||||
channelClient.getOutputStream(channel).await().use {
|
||||
val output = DataOutputStream(it)
|
||||
output.writeInt(firmware.size)
|
||||
transferredBytes += 4
|
||||
transferPercent.value = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
|
||||
val buffer = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
firmware.inputStream().use { firmwareInput ->
|
||||
while (firmwareInput.read(buffer).also { bytesRead = it } >= 0) {
|
||||
output.write(buffer, 0, bytesRead)
|
||||
transferredBytes += bytesRead
|
||||
transferPercent.value = if (totalBytes == 0L) 0 else ((transferredBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
|
||||
}
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
} finally {
|
||||
runCatching { channelClient.close(channel).await() }
|
||||
}
|
||||
}
|
||||
transferStatus.value = "Transfer complete"
|
||||
transferPercent.value = 100
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@CompanionFirmwareImportActivity, getString(R.string.companion_firmware_sent_success), Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to send firmware to watch")
|
||||
transferStatus.value = "Transfer failed: ${e.message ?: "unknown"}"
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@CompanionFirmwareImportActivity, "Firmware transfer failed: ${e.message ?: "unknown error"}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
importState.value = FirmwareImportState.PickFirmware
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.github.nacabaro.vbhelper.companion.logging
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.tinylog.Logger
|
||||
import org.tinylog.configuration.Configuration
|
||||
import org.tinylog.provider.ProviderRegistry
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("LogNotTimber")
|
||||
class TinyLogTree(context: Context) : Timber.DebugTree() {
|
||||
companion object {
|
||||
private val logFileRegexp = Regex("""log_(\d{4}-\d{2}-\d{2})_(\d+)\.txt""")
|
||||
private const val START_DATE = "0000-00-00"
|
||||
|
||||
fun shutdown() {
|
||||
ProviderRegistry.getLoggingProvider().shutdown()
|
||||
}
|
||||
|
||||
private fun setupRollingLogs(logsDir: String) {
|
||||
Configuration.set("writer", "rolling file")
|
||||
Configuration.set("writer.level", "info")
|
||||
Configuration.set("writer.backups", "3")
|
||||
Configuration.set("writer.format", "{date: yyyy-MM-dd HH:mm:ss.SSS}{pipe}{tag}{pipe}{level}{pipe}{message}")
|
||||
Configuration.set("writer.file", "$logsDir/log_{date:yyyy-MM-dd}_{count}.txt")
|
||||
Configuration.set("writer.policies", "daily, size: 10mb")
|
||||
Configuration.set("writer.buffered", "true")
|
||||
}
|
||||
|
||||
private fun getLogsDir(context: Context): File = File(context.filesDir, "logs")
|
||||
|
||||
fun getMostRecentLogFile(context: Context): File? {
|
||||
val logsDir = getLogsDir(context)
|
||||
val files = logsDir.listFiles() ?: return null
|
||||
return getMostRecentLogFileFromFiles(files)
|
||||
}
|
||||
|
||||
internal fun getMostRecentLogFileFromFiles(files: Array<out File>): File? {
|
||||
var date = START_DATE
|
||||
var count = 0
|
||||
var mostRecentFile: File? = null
|
||||
for (child in files) {
|
||||
val matchResult = logFileRegexp.find(child.name) ?: continue
|
||||
val childDate = matchResult.groups[1]?.value ?: START_DATE
|
||||
val childCount = matchResult.groups[2]?.value?.toInt() ?: 0
|
||||
if (childDate > date || (childDate == date && childCount > count)) {
|
||||
mostRecentFile = child
|
||||
date = childDate
|
||||
count = childCount
|
||||
}
|
||||
}
|
||||
Log.i("TinyLogTree", "Most Recent Log: $mostRecentFile")
|
||||
return mostRecentFile
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val logsDir = getLogsDir(context).absolutePath
|
||||
setupRollingLogs(logsDir)
|
||||
Configuration.set("writingthread", "true")
|
||||
Log.i("TinyLogTree", "Log Dir: $logsDir")
|
||||
}
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
val taggedLogger = Logger.tag(tag)
|
||||
when (priority) {
|
||||
Log.VERBOSE, Log.DEBUG -> taggedLogger.debug { message }
|
||||
Log.INFO -> taggedLogger.info { message }
|
||||
Log.WARN -> taggedLogger.warn { message }
|
||||
Log.ERROR, Log.ASSERT -> taggedLogger.error { message }
|
||||
else -> taggedLogger.error { "Invalid priority $priority For Message! $message" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.github.nacabaro.vbhelper.companion.logs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
|
||||
import com.google.android.gms.wearable.CapabilityClient
|
||||
import com.google.android.gms.wearable.ChannelClient
|
||||
import com.google.android.gms.wearable.ChannelClient.ChannelCallback
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
class CompanionLogService {
|
||||
companion object {
|
||||
private const val TAG = "CompanionLogService"
|
||||
}
|
||||
|
||||
var initializedActivity: Activity? = null
|
||||
|
||||
fun sendLogFile(context: Context, file: File) {
|
||||
initializedActivity?.let {
|
||||
sendLogFile(context, file, it)
|
||||
initializedActivity?.finish()
|
||||
}
|
||||
initializedActivity = null
|
||||
}
|
||||
|
||||
@SuppressLint("LogNotTimber")
|
||||
fun sendLogFile(context: Context, file: File, activity: Activity) {
|
||||
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = Uri.parse("mailto:")
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf("vitalwear-developers@googlegroups.com", "taveonnelsonn@gmail.com"))
|
||||
putExtra(Intent.EXTRA_SUBJECT, "VBHelper Log")
|
||||
putExtra(Intent.EXTRA_TEXT, "Please take a look at these logs to help diagnose the below issue:\n")
|
||||
}
|
||||
val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
||||
emailIntent.putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
emailIntent.clipData = ClipData.newRawUri("", fileUri)
|
||||
emailIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
Log.i(TAG, "Starting activity to send email")
|
||||
activity.startActivity(Intent.createChooser(emailIntent, "Select Email Client"))
|
||||
}
|
||||
|
||||
fun receiveFile(context: Context, channel: ChannelClient.Channel) {
|
||||
val channelClient = Wearable.getChannelClient(context)
|
||||
val logs = File(context.filesDir, "logs")
|
||||
if (!logs.exists()) {
|
||||
logs.mkdirs()
|
||||
}
|
||||
val file = File(logs, "watch_log.txt")
|
||||
channelClient.registerChannelCallback(channel, object : ChannelCallback() {
|
||||
override fun onInputClosed(channel: ChannelClient.Channel, closeReason: Int, appErrorCode: Int) {
|
||||
super.onInputClosed(channel, closeReason, appErrorCode)
|
||||
if (closeReason != ChannelCallback.CLOSE_REASON_NORMAL) {
|
||||
Timber.w("Failed to receive file. Close Reason: ${closeReasonString(closeReason)}")
|
||||
} else {
|
||||
Timber.i("Successfully downloaded log file. Input closed.")
|
||||
sendLogFile(context, file)
|
||||
}
|
||||
channelClient.close(channel)
|
||||
}
|
||||
})
|
||||
channelClient.receiveFile(channel, Uri.fromFile(file), false).apply {
|
||||
addOnFailureListener {
|
||||
Toast.makeText(context, context.getString(R.string.companion_logs_receive_failed), Toast.LENGTH_SHORT).show()
|
||||
initializedActivity?.finish()
|
||||
initializedActivity = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchWatchLogs(callingActivity: Activity) {
|
||||
val messageClient = Wearable.getMessageClient(callingActivity)
|
||||
val capabilityInfoTask = Wearable.getCapabilityClient(callingActivity)
|
||||
.getCapability(ChannelTypes.SEND_LOGS_REQUEST, CapabilityClient.FILTER_REACHABLE)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val capabilityInfo = capabilityInfoTask.await()
|
||||
if (capabilityInfo.nodes.isEmpty()) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
Toast.makeText(callingActivity, callingActivity.getString(R.string.companion_firmware_no_watch), Toast.LENGTH_SHORT).show()
|
||||
callingActivity.finish()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
for (node in capabilityInfo.nodes) {
|
||||
messageClient.sendMessage(node.id, ChannelTypes.SEND_LOGS_REQUEST, null).apply {
|
||||
addOnFailureListener {
|
||||
Toast.makeText(callingActivity, callingActivity.getString(R.string.companion_logs_failed_request), Toast.LENGTH_SHORT).show()
|
||||
callingActivity.finish()
|
||||
}
|
||||
addOnSuccessListener {
|
||||
initializedActivity = callingActivity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeReasonString(closeReason: Int): String {
|
||||
return when (closeReason) {
|
||||
ChannelCallback.CLOSE_REASON_NORMAL -> "Normal"
|
||||
ChannelCallback.CLOSE_REASON_LOCAL_CLOSE -> "Closed Locally"
|
||||
ChannelCallback.CLOSE_REASON_REMOTE_CLOSE -> "Closed Remotely"
|
||||
ChannelCallback.CLOSE_REASON_DISCONNECTED -> "Disconnected"
|
||||
ChannelCallback.CLOSE_REASON_CONNECTION_TIMEOUT -> "Timeout"
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.github.nacabaro.vbhelper.companion.logs
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.companion.ui.CompanionConfirmation
|
||||
import com.github.nacabaro.vbhelper.companion.ui.CompanionLoading
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
|
||||
class CompanionWatchLogsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val activity = this
|
||||
setContent {
|
||||
var accepted by remember { mutableStateOf(false) }
|
||||
if (!accepted) {
|
||||
CompanionConfirmation(prompt = stringResource(R.string.companion_logs_warning)) {
|
||||
if (it) {
|
||||
accepted = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CompanionLoading(loadingText = stringResource(R.string.settings_companion_send_watch_logs_title))
|
||||
LaunchedEffect(true) {
|
||||
(application as VBHelper).companionLogService.fetchWatchLogs(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.github.nacabaro.vbhelper.companion.logs
|
||||
|
||||
import com.github.nacabaro.vbhelper.companion.common.ChannelTypes
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.google.android.gms.wearable.ChannelClient
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
import timber.log.Timber
|
||||
|
||||
class WatchCommunicationService : WearableListenerService() {
|
||||
override fun onChannelOpened(channel: ChannelClient.Channel) {
|
||||
super.onChannelOpened(channel)
|
||||
when (channel.path) {
|
||||
ChannelTypes.LOGS_DATA -> {
|
||||
val logService = (application as VBHelper).companionLogService
|
||||
logService.receiveFile(this, channel)
|
||||
}
|
||||
else -> Timber.i("Unknown channel: ${channel.path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.github.nacabaro.vbhelper.companion.transfer
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
|
||||
/**
|
||||
* Compatibility endpoint kept for legacy links/intents from older companion flows.
|
||||
* Character transfer remains watch-driven and is not supported from phone companion UI.
|
||||
*/
|
||||
class CompanionCharacterImportActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.companion_transfer_disabled_message),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.github.nacabaro.vbhelper.companion.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CompanionConfirmation(
|
||||
prompt: String,
|
||||
onResult: (Boolean) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = prompt, modifier = Modifier.padding(bottom = 24.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Button(onClick = { onResult(true) }) {
|
||||
Text(text = "Ok")
|
||||
}
|
||||
Button(onClick = { onResult(false) }) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.github.nacabaro.vbhelper.companion.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CompanionLoading(
|
||||
loadingText: String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = loadingText,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.github.nacabaro.vbhelper.companion.validation
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [ValidatedCardEntity::class],
|
||||
version = 1,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class CompanionValidatedDatabase : RoomDatabase() {
|
||||
abstract fun validatedCardDao(): ValidatedCardDao
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.github.nacabaro.vbhelper.companion.validation
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface ValidatedCardDao {
|
||||
@Query("select cardId from ${ValidatedCardEntity.TABLE}")
|
||||
suspend fun getIds(): List<Int>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insert(validatedCardEntity: ValidatedCardEntity)
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.nacabaro.vbhelper.companion.validation
|
||||
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = ValidatedCardEntity.TABLE, primaryKeys = ["cardId"])
|
||||
data class ValidatedCardEntity(
|
||||
val cardId: Int,
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE = "validated"
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.github.nacabaro.vbhelper.companion.validation
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class ValidatedCardManager(
|
||||
private val validatedCardDao: ValidatedCardDao,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private var validatedIds: Set<Int>? = null
|
||||
|
||||
private suspend fun ensureLoaded(): Set<Int> {
|
||||
validatedIds?.let { return it }
|
||||
return mutex.withLock {
|
||||
validatedIds ?: validatedCardDao.getIds().toSet().also { validatedIds = it }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addValidatedCard(cardId: Int) {
|
||||
mutex.withLock {
|
||||
val current = validatedIds ?: validatedCardDao.getIds().toSet()
|
||||
if (!current.contains(cardId)) {
|
||||
validatedCardDao.insert(ValidatedCardEntity(cardId))
|
||||
validatedIds = current + cardId
|
||||
} else {
|
||||
validatedIds = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isValidatedCard(cardId: Int): Boolean {
|
||||
return ensureLoaded().contains(cardId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,15 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -28,21 +32,31 @@ import com.github.nacabaro.vbhelper.utils.getBitmap
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import com.github.cfogrady.vbnfc.vb.SpecialMission
|
||||
import com.github.nacabaro.vbhelper.R
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import com.github.nacabaro.vbhelper.utils.getObscuredBitmap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
|
||||
@Composable
|
||||
fun CharacterEntry(
|
||||
icon: BitmapData,
|
||||
modifier: Modifier = Modifier,
|
||||
cardIcon: BitmapData? = null,
|
||||
obscure: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
shape: Shape = MaterialTheme.shapes.medium,
|
||||
multiplier: Int = 4,
|
||||
cardColors: CardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
),
|
||||
onClick: () -> Unit = { }
|
||||
) {
|
||||
val bitmap = remember (icon.bitmap) {
|
||||
if(obscure) icon.getObscuredBitmap() else icon.getBitmap()
|
||||
}
|
||||
val iconSizeMultiplier = 3
|
||||
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
|
||||
val density: Float = LocalContext.current.resources.displayMetrics.density
|
||||
val dpSize = (icon.width * multiplier / density).dp
|
||||
@@ -55,7 +69,8 @@ fun CharacterEntry(
|
||||
},
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.padding(8.dp)
|
||||
.padding(8.dp),
|
||||
colors = cardColors
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
@@ -73,7 +88,24 @@ fun CharacterEntry(
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(dpSize)
|
||||
.align(Alignment.BottomCenter)
|
||||
)
|
||||
|
||||
if (cardIcon != null) {
|
||||
val bitmap = remember (icon.bitmap) { cardIcon.getBitmap() }
|
||||
val iconBitmap = remember(bitmap) { bitmap.asImageBitmap() }
|
||||
val dpSize = (icon.width * iconSizeMultiplier /density).dp
|
||||
|
||||
Image(
|
||||
bitmap = iconBitmap,
|
||||
contentDescription = "Card icon",
|
||||
filterQuality = FilterQuality.None,
|
||||
modifier = Modifier
|
||||
.size(dpSize)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,3 +147,116 @@ fun ItemDisplay(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpecialMissionsEntry(
|
||||
specialMission: SpecialMissions,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickMission: (Long) -> Unit = { },
|
||||
onClickCollect: (Long) -> Unit = { }
|
||||
) {
|
||||
val textValue = when (specialMission.missionType) {
|
||||
SpecialMission.Type.NONE -> stringResource(R.string.special_mission_none)
|
||||
SpecialMission.Type.STEPS -> stringResource(
|
||||
R.string.special_mission_steps,
|
||||
specialMission.goal
|
||||
)
|
||||
SpecialMission.Type.BATTLES -> stringResource(
|
||||
R.string.special_mission_battles,
|
||||
specialMission.goal
|
||||
)
|
||||
SpecialMission.Type.WINS -> stringResource(
|
||||
R.string.special_mission_wins,
|
||||
specialMission.goal
|
||||
)
|
||||
SpecialMission.Type.VITALS -> stringResource(
|
||||
R.string.special_mission_vitals,
|
||||
specialMission.goal
|
||||
)
|
||||
}
|
||||
|
||||
val progress = if (specialMission.status == SpecialMission.Status.COMPLETED) {
|
||||
specialMission.goal
|
||||
} else {
|
||||
specialMission.progress
|
||||
}
|
||||
|
||||
val completion = when (specialMission.missionType) {
|
||||
SpecialMission.Type.NONE -> ""
|
||||
SpecialMission.Type.STEPS -> stringResource(
|
||||
R.string.special_mission_steps_progress,
|
||||
progress
|
||||
)
|
||||
SpecialMission.Type.BATTLES -> stringResource(
|
||||
R.string.special_mission_battles_progress,
|
||||
progress
|
||||
)
|
||||
SpecialMission.Type.WINS -> stringResource(
|
||||
R.string.special_mission_wins_progress,
|
||||
progress
|
||||
)
|
||||
SpecialMission.Type.VITALS -> stringResource(
|
||||
R.string.special_mission_vitals_progress,
|
||||
progress
|
||||
)
|
||||
}
|
||||
|
||||
val icon = when (specialMission.missionType) {
|
||||
SpecialMission.Type.NONE -> R.drawable.baseline_free_24
|
||||
SpecialMission.Type.STEPS -> R.drawable.baseline_agility_24
|
||||
SpecialMission.Type.BATTLES -> R.drawable.baseline_swords_24
|
||||
SpecialMission.Type.WINS -> R.drawable.baseline_trophy_24
|
||||
SpecialMission.Type.VITALS -> R.drawable.baseline_vitals_24
|
||||
}
|
||||
|
||||
val color = when (specialMission.status)
|
||||
{
|
||||
SpecialMission.Status.IN_PROGRESS -> MaterialTheme.colorScheme.secondary
|
||||
SpecialMission.Status.COMPLETED -> MaterialTheme.colorScheme.primary
|
||||
SpecialMission.Status.FAILED -> MaterialTheme.colorScheme.error
|
||||
else -> MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = androidx.compose.material.MaterialTheme.shapes.small,
|
||||
onClick = if (specialMission.status == SpecialMission.Status.COMPLETED) {
|
||||
{ onClickCollect(specialMission.id) }
|
||||
} else if (specialMission.status == SpecialMission.Status.UNAVAILABLE) {
|
||||
{ }
|
||||
} else {
|
||||
{ onClickMission(specialMission.id) }
|
||||
},
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = color
|
||||
)
|
||||
|
||||
) {
|
||||
Row (
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = stringResource(R.string.special_mission_icon_content_description),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(16.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = textValue,
|
||||
fontFamily = MaterialTheme.typography.titleLarge.fontFamily,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = completion,
|
||||
fontFamily = MaterialTheme.typography.titleSmall.fontFamily,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ fun TopBanner(
|
||||
onGearClick: (() -> Unit)? = null,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
onScanClick: (() -> Unit)? = null,
|
||||
onAdventureClick: (() -> Unit)? = null
|
||||
onAdventureClick: (() -> Unit)? = null,
|
||||
onModifyClick: (() -> Unit)? = null
|
||||
) {
|
||||
Box( // Use Box to overlay elements
|
||||
modifier = modifier
|
||||
@@ -37,16 +38,16 @@ fun TopBanner(
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 24.sp,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center) // Center the text
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
if (onGearClick != null) {
|
||||
IconButton(
|
||||
onClick = onGearClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd) // Place gear icon at the end
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.baseline_settings_24), // Use a gear icon
|
||||
painter = painterResource(R.drawable.baseline_settings_24),
|
||||
contentDescription = "Settings"
|
||||
)
|
||||
}
|
||||
@@ -54,10 +55,21 @@ fun TopBanner(
|
||||
IconButton(
|
||||
onClick = onAdventureClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd) // Place gear icon at the end
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.baseline_fort_24), // Use a gear icon
|
||||
painter = painterResource(R.drawable.baseline_fort_24),
|
||||
contentDescription = "Adventure"
|
||||
)
|
||||
}
|
||||
} else if (onModifyClick != null) {
|
||||
IconButton(
|
||||
onClick = onModifyClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.baseline_edit_24),
|
||||
contentDescription = "Adventure"
|
||||
)
|
||||
}
|
||||
@@ -67,10 +79,10 @@ fun TopBanner(
|
||||
IconButton(
|
||||
onClick = onScanClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart) // Place gear icon at the end
|
||||
.align(Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.baseline_nfc_24), // Use a gear icon
|
||||
painter = painterResource(R.drawable.baseline_nfc_24),
|
||||
contentDescription = "Scan"
|
||||
)
|
||||
}
|
||||
@@ -78,10 +90,10 @@ fun TopBanner(
|
||||
IconButton(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart) // Place gear icon at the end
|
||||
.align(Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.baseline_arrow_back_24), // Use a gear icon
|
||||
painter = painterResource(R.drawable.baseline_arrow_back_24),
|
||||
contentDescription = "Settings"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
@Dao
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
interface AdventureDao {
|
||||
@Query("""
|
||||
INSERT INTO Adventure (characterId, originalDuration, finishesAdventure)
|
||||
@@ -18,23 +21,26 @@ interface AdventureDao {
|
||||
""")
|
||||
fun getAdventureCount(): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.finishesAdventure AS finishesAdventure,
|
||||
a.originalDuration AS originalTimeInMinutes
|
||||
FROM UserCharacter uc
|
||||
JOIN Character c ON uc.charId = c.id
|
||||
JOIN Card d ON c.dimId = d.id
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
JOIN Card d ON c.cardId = d.id
|
||||
JOIN Adventure a ON uc.id = a.characterId
|
||||
""")
|
||||
suspend fun getAdventureCharacters(): List<CharacterDtos.AdventureCharacterWithSprites>
|
||||
"""
|
||||
)
|
||||
fun getAdventureCharacters(): Flow<List<CharacterDtos.AdventureCharacterWithSprites>>
|
||||
|
||||
@Query("""
|
||||
DELETE FROM Adventure
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CardAdventureDao {
|
||||
@Query("""
|
||||
INSERT INTO
|
||||
CardAdventure (cardId, characterId, steps, bossAp, bossHp, bossDp, bossBp)
|
||||
SELECT
|
||||
:cardId,
|
||||
cc.id,
|
||||
:steps,
|
||||
:bossAp,
|
||||
:bossHp,
|
||||
:bossDp,
|
||||
:bossBp
|
||||
FROM
|
||||
CardCharacter cc
|
||||
WHERE
|
||||
cc.charaIndex = :characterId AND
|
||||
cc.cardId = :cardId
|
||||
""")
|
||||
suspend fun insertNewAdventure(
|
||||
cardId: Long,
|
||||
characterId: Int,
|
||||
steps: Int,
|
||||
bossAp: Int,
|
||||
bossHp: Int,
|
||||
bossDp: Int,
|
||||
bossBp: Int?
|
||||
)
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
cc.nameSprite as characterName,
|
||||
cc.nameWidth as characterNameWidth,
|
||||
cc.nameHeight as characterNameHeight,
|
||||
s.spriteIdle1 as characterIdleSprite,
|
||||
s.width as characterIdleSpriteWidth,
|
||||
s.height as characterIdleSpriteHeight,
|
||||
ca.bossAp as characterAp,
|
||||
ca.bossBp as characterBp,
|
||||
ca.bossDp as characterDp,
|
||||
ca.bossHp as characterHp,
|
||||
ca.steps as steps
|
||||
FROM CardCharacter cc
|
||||
JOIN Sprite s ON cc.spriteId = s.id
|
||||
JOIN CardAdventure ca ON cc.id = ca.characterId
|
||||
WHERE
|
||||
cc.cardId = :cardId
|
||||
""")
|
||||
fun getAdventureForCard(
|
||||
cardId: Long
|
||||
): Flow<List<CardDtos.CardAdventureWithSprites>>
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.card.Card
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CardDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertNewCard(card: Card): Long
|
||||
|
||||
@Query("SELECT * FROM Card")
|
||||
fun getAllCards(): List<Card>
|
||||
|
||||
@Query("SELECT * FROM Card WHERE cardId = :id")
|
||||
fun getCardByCardId(id: Int): List<Card>
|
||||
|
||||
@Query("SELECT * FROM Card WHERE id = :id")
|
||||
fun getCardById(id: Long): Card?
|
||||
|
||||
@Query("SELECT * FROM Card WHERE LOWER(name) = LOWER(:name) LIMIT 1")
|
||||
fun getCardByName(name: String): Card?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT ca.*
|
||||
FROM Card ca
|
||||
JOIN CardCharacter ch ON ca.id = ch.cardId
|
||||
JOIN UserCharacter uc ON ch.id = uc.charId
|
||||
WHERE uc.id = :id
|
||||
"""
|
||||
)
|
||||
fun getCardByCharacterId(id: Long): Flow<Card>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT ca.*
|
||||
FROM Card ca
|
||||
JOIN CardCharacter ch ON ca.id = ch.cardId
|
||||
JOIN UserCharacter uc ON ch.id = uc.charId
|
||||
WHERE uc.id = :id
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
fun getCardByCharacterIdSync(id: Long): Card?
|
||||
|
||||
@Query("UPDATE Card SET name = :newName WHERE id = :id")
|
||||
suspend fun renameCard(id: Int, newName: String)
|
||||
|
||||
@Query("DELETE FROM Card WHERE id = :id")
|
||||
suspend fun deleteCard(id: Long)
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
c.logo as cardIcon,
|
||||
c.logoWidth as cardIconWidth,
|
||||
c.logoHeight as cardIconHeight
|
||||
FROM Card c
|
||||
JOIN CardCharacter cc ON cc.cardId = c.id
|
||||
WHERE cc.id = :charaId
|
||||
""")
|
||||
fun getCardIconByCharaId(charaId: Long): Flow<CardDtos.CardIcon>
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
interface CardFusionsDao {
|
||||
@Query("""
|
||||
INSERT INTO
|
||||
CardFusions (
|
||||
fromCharaId,
|
||||
attribute,
|
||||
toCharaId
|
||||
)
|
||||
SELECT
|
||||
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :fromCharaId),
|
||||
:attribute,
|
||||
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaId)
|
||||
""")
|
||||
suspend fun insertNewFusion(
|
||||
cardId: Long,
|
||||
fromCharaId: Int,
|
||||
attribute: NfcCharacter.Attribute,
|
||||
toCharaId: Int
|
||||
)
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
cf.toCharaId as charaId,
|
||||
cf.fromCharaId as fromCharaId,
|
||||
s.spriteIdle1 as spriteIdle,
|
||||
cc.attribute as attribute,
|
||||
s.width as spriteWidth,
|
||||
s.height as spriteHeight,
|
||||
d.discoveredOn as discoveredOn,
|
||||
cf.attribute as fusionAttribute
|
||||
FROM CardFusions cf
|
||||
JOIN CardCharacter cc ON cc.id = cf.toCharaId
|
||||
JOIN Sprite s ON s.id = cc.id
|
||||
LEFT JOIN Dex d ON d.id = cc.id
|
||||
WHERE cf.fromCharaId = :charaId
|
||||
ORDER BY cc.charaIndex
|
||||
""")
|
||||
fun getFusionsForCharacter(charaId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardProgress
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CardProgressDao {
|
||||
@Query("""
|
||||
UPDATE CardProgress
|
||||
SET
|
||||
currentStage = :currentStage,
|
||||
unlocked = :unlocked
|
||||
WHERE cardId = :cardId AND
|
||||
currentStage < :currentStage
|
||||
""")
|
||||
fun updateCardProgress(currentStage: Int, cardId: Long, unlocked: Boolean)
|
||||
|
||||
@Query(
|
||||
"SELECT currentStage FROM CardProgress WHERE cardId = :cardId"
|
||||
)
|
||||
fun getCardProgress(cardId: Long): Flow<Int>
|
||||
|
||||
@Query("SELECT currentStage FROM CardProgress WHERE cardId = :cardId LIMIT 1")
|
||||
fun getCardProgressSync(cardId: Long): Int?
|
||||
|
||||
@Insert
|
||||
fun insertCardProgress(cardProgress: CardProgress)
|
||||
}
|
||||
@@ -3,49 +3,88 @@ package com.github.nacabaro.vbhelper.daos
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.Sprites
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Sprite
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CharacterDao {
|
||||
@Insert
|
||||
suspend fun insertCharacter(vararg characterData: Character)
|
||||
suspend fun insertCharacter(vararg characterData: CardCharacter)
|
||||
|
||||
@Query("SELECT * FROM Character")
|
||||
suspend fun getAllCharacters(): List<Character>
|
||||
|
||||
@Query("SELECT * FROM Character WHERE dimId = :dimId")
|
||||
suspend fun getCharacterByDimId(dimId: Int): List<Character>
|
||||
|
||||
@Query("SELECT * FROM Character WHERE monIndex = :monIndex AND dimId = :dimId LIMIT 1")
|
||||
fun getCharacterByMonIndex(monIndex: Int, dimId: Long): Character
|
||||
@Query("SELECT * FROM CardCharacter WHERE charaIndex = :monIndex AND cardId = :dimId LIMIT 1")
|
||||
fun getCharacterByMonIndex(monIndex: Int, dimId: Long): CardCharacter
|
||||
|
||||
@Insert
|
||||
suspend fun insertSprite(vararg sprite: Sprites)
|
||||
|
||||
@Query("SELECT * FROM Sprites")
|
||||
suspend fun getAllSprites(): List<Sprites>
|
||||
suspend fun insertSprite(vararg sprite: Sprite)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
d.dimId as cardId,
|
||||
c.monIndex as charId
|
||||
FROM Character c
|
||||
d.cardId as cardId,
|
||||
c.charaIndex as charId,
|
||||
c.stage as stage,
|
||||
c.attribute as attribute,
|
||||
cp.currentStage as currentStage
|
||||
FROM CardCharacter c
|
||||
JOIN UserCharacter uc ON c.id = uc.charId
|
||||
JOIN Card d ON c.dimId = d.id
|
||||
WHERE uc.id = :charId
|
||||
JOIN Card d ON c.cardId = d.id
|
||||
JOIN CardProgress cp ON d.id = cp.cardId
|
||||
WHERE c.id = :charId
|
||||
"""
|
||||
)
|
||||
suspend fun getCharacterInfo(charId: Long): CharacterDtos.DiMInfo
|
||||
suspend fun getCharacterInfo(charId: Long): CharacterDtos.CardCharacterInfo
|
||||
|
||||
@Query("""
|
||||
INSERT INTO TransformationHistory(monId, stageId, transformationDate)
|
||||
VALUES
|
||||
(:monId,
|
||||
(SELECT id FROM Character WHERE monIndex = :stage AND dimId = :dimId),
|
||||
:transformationDate)
|
||||
""")
|
||||
fun insertTransformation(monId: Long, stage: Int, dimId: Long, transformationDate: Long)
|
||||
@Query(
|
||||
"""
|
||||
INSERT INTO PossibleTransformations (charaId, requiredVitals, requiredTrophies, requiredBattles, requiredWinRate, changeTimerHours, requiredAdventureLevelCompleted, toCharaId)
|
||||
SELECT
|
||||
(SELECT id FROM CardCharacter WHERE charaIndex = :fromChraraIndex AND cardId = :cardId),
|
||||
:requiredVitals,
|
||||
:requiredTrophies,
|
||||
:requiredBattles,
|
||||
:requiredWinRate,
|
||||
:changeTimerHours,
|
||||
:requiredAdventureLevelCompleted,
|
||||
(SELECT id FROM CardCharacter WHERE charaIndex = :toChraraIndex AND cardId = :cardId)
|
||||
"""
|
||||
)
|
||||
suspend fun insertPossibleTransformation(
|
||||
fromChraraIndex: Int,
|
||||
toChraraIndex: Int,
|
||||
cardId: Long,
|
||||
requiredVitals: Int,
|
||||
requiredTrophies: Int,
|
||||
requiredBattles: Int,
|
||||
changeTimerHours: Int,
|
||||
requiredWinRate: Int,
|
||||
requiredAdventureLevelCompleted: Int
|
||||
)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
pt.charaId as fromCharaId,
|
||||
pt.toCharaId as charaId,
|
||||
s.spriteIdle1 as spriteIdle,
|
||||
s.width as spriteWidth,
|
||||
s.height as spriteHeight,
|
||||
d.discoveredOn as discoveredOn,
|
||||
pt.requiredTrophies as requiredTrophies,
|
||||
pt.requiredVitals as requiredVitals,
|
||||
pt.requiredBattles as requiredBattles,
|
||||
pt.requiredWinRate as requiredWinRate,
|
||||
pt.changeTimerHours as changeTimerHours,
|
||||
pt.requiredAdventureLevelCompleted as requiredAdventureLevelCompleted
|
||||
FROM
|
||||
PossibleTransformations pt
|
||||
JOIN CardCharacter c on pt.toCharaId = c.id
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Dex d ON d.id = pt.toCharaId
|
||||
WHERE
|
||||
pt.charaId = :characterId
|
||||
"""
|
||||
)
|
||||
fun getEvolutionRequirementsForCard(characterId: Long): Flow<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
|
||||
|
||||
@Dao
|
||||
interface CharacterTransferPolicyDao {
|
||||
@Query("SELECT * FROM CharacterTransferPolicy WHERE characterId = :characterId")
|
||||
fun getByCharacterId(characterId: Long): CharacterTransferPolicy?
|
||||
|
||||
@Upsert
|
||||
fun upsert(policy: CharacterTransferPolicy)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,41 +4,57 @@ import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.dtos.CardDtos
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface DexDao {
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
INSERT OR IGNORE INTO Dex(id, discoveredOn)
|
||||
VALUES (
|
||||
(SELECT id FROM Character WHERE monIndex = :charIndex AND dimId = :cardId),
|
||||
(SELECT id FROM CardCharacter WHERE charaIndex = :charIndex AND cardId = :cardId),
|
||||
:discoveredOn
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun insertCharacter(charIndex: Int, cardId: Long, discoveredOn: Long)
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
c.id AS id,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
d.discoveredOn AS discoveredOn
|
||||
FROM character c
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite AS nameSprite,
|
||||
c.nameWidth AS nameSpriteWidth,
|
||||
c.nameHeight AS nameSpriteHeight,
|
||||
d.discoveredOn AS discoveredOn,
|
||||
c.baseHp as baseHp,
|
||||
c.baseBp as baseBp,
|
||||
c.baseAp as baseAp,
|
||||
c.stage as stage,
|
||||
c.attribute as attribute
|
||||
FROM CardCharacter c
|
||||
JOIN Sprite s ON c.spriteId = s.id
|
||||
LEFT JOIN dex d ON c.id = d.id
|
||||
WHERE c.dimId = :cardId
|
||||
""")
|
||||
suspend fun getSingleCardProgress(cardId: Long): List<CharacterDtos.CardProgress>
|
||||
WHERE c.cardId = :cardId
|
||||
"""
|
||||
)
|
||||
fun getSingleCardProgress(cardId: Long): Flow<List<CharacterDtos.CardCharaProgress>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
c.id as cardId,
|
||||
c.name as cardName,
|
||||
c.logo as cardLogo,
|
||||
c.logoWidth as logoWidth,
|
||||
c.logoHeight as logoHeight,
|
||||
(SELECT COUNT(*) FROM Character cc WHERE cc.dimId = c.id) AS totalCharacters,
|
||||
(SELECT COUNT(*) FROM Dex d JOIN Character cc ON d.id = cc.id WHERE cc.dimId = c.id AND d.discoveredOn IS NOT NULL) AS obtainedCharacters
|
||||
(SELECT COUNT(*) FROM CardCharacter cc WHERE cc.cardId = c.id) AS totalCharacters,
|
||||
(SELECT COUNT(*) FROM Dex d JOIN CardCharacter cc ON d.id = cc.id WHERE cc.cardId = c.id AND d.discoveredOn IS NOT NULL) AS obtainedCharacters
|
||||
FROM Card c
|
||||
""")
|
||||
suspend fun getCardsWithProgress(): List<CardDtos.CardProgress>
|
||||
"""
|
||||
)
|
||||
fun getCardsWithProgress(): Flow<List<CardDtos.CardProgress>>
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
|
||||
@Dao
|
||||
interface DiMDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertNewDim(card: Card): Long
|
||||
|
||||
@Query("SELECT * FROM Card")
|
||||
suspend fun getAllDims(): List<Card>
|
||||
|
||||
@Query("SELECT * FROM Card WHERE dimId = :id")
|
||||
fun getDimById(id: Int): Card?
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Upsert
|
||||
import com.github.nacabaro.vbhelper.domain.DimProgress
|
||||
|
||||
@Dao
|
||||
interface DiMProgressDao {
|
||||
@Upsert
|
||||
suspend fun updateDimProgress(vararg dimProgress: DimProgress)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.daos
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.dtos.ItemDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ItemDao {
|
||||
@@ -13,7 +14,7 @@ interface ItemDao {
|
||||
ORDER BY Items.itemIcon ASC
|
||||
"""
|
||||
)
|
||||
suspend fun getAllItems(): List<ItemDtos.ItemsWithQuantities>
|
||||
fun getAllItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -22,7 +23,7 @@ interface ItemDao {
|
||||
WHERE quantity > 0
|
||||
"""
|
||||
)
|
||||
suspend fun getAllUserItems(): List<ItemDtos.ItemsWithQuantities>
|
||||
fun getAllUserItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -31,7 +32,7 @@ interface ItemDao {
|
||||
WHERE Items.id = :itemId
|
||||
"""
|
||||
)
|
||||
fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities
|
||||
suspend fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -40,7 +41,7 @@ interface ItemDao {
|
||||
WHERE id = :itemId
|
||||
"""
|
||||
)
|
||||
fun useItem(itemId: Long)
|
||||
suspend fun useItem(itemId: Long)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SpecialMissionDao {
|
||||
@Query("""
|
||||
UPDATE SpecialMissions SET
|
||||
missionType = "NONE",
|
||||
status = "UNAVAILABLE"
|
||||
WHERE id = :id
|
||||
""")
|
||||
suspend fun clearSpecialMission(id: Long)
|
||||
|
||||
@Query("""
|
||||
SELECT *
|
||||
FROM SpecialMissions
|
||||
WHERE id = :id
|
||||
""")
|
||||
fun getSpecialMission(id: Long): Flow<SpecialMissions>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Sprite
|
||||
|
||||
@Dao
|
||||
interface SpriteDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertSprite(sprite: Sprite): Long
|
||||
|
||||
@Query("SELECT * FROM Sprite")
|
||||
suspend fun getAllSprites(): List<Sprite>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenDao
|
||||
|
||||
@Deprecated("Use SharedTransferSeenDao from the shared transfer database.")
|
||||
typealias TransferSeenDao = SharedTransferSeenDao
|
||||
|
||||
@@ -5,13 +5,19 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalsHistory
|
||||
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
interface UserCharacterDao {
|
||||
@Insert
|
||||
fun insertCharacterData(characterData: UserCharacter): Long
|
||||
@@ -19,6 +25,9 @@ interface UserCharacterDao {
|
||||
@Insert
|
||||
fun insertBECharacterData(characterData: BECharacterData)
|
||||
|
||||
@Insert
|
||||
fun insertVBCharacterData(characterData: VBCharacterData)
|
||||
|
||||
@Upsert
|
||||
fun updateCharacter(character: UserCharacter)
|
||||
|
||||
@@ -28,19 +37,41 @@ interface UserCharacterDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertTransformationHistory(vararg transformationHistory: TransformationHistory)
|
||||
|
||||
@Query("""
|
||||
@Upsert
|
||||
fun insertSpecialMissions(vararg specialMissions: SpecialMissions)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
c.id AS id,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
c.monIndex AS monIndex,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.charaIndex AS monIndex,
|
||||
t.transformationDate AS transformationDate
|
||||
FROM TransformationHistory t
|
||||
JOIN Character c ON c.id = t.stageId
|
||||
JOIN CardCharacter c ON c.id = t.stageId
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
WHERE monId = :monId
|
||||
""")
|
||||
fun getTransformationHistory(monId: Long): List<CharacterDtos.TransformationHistory>?
|
||||
"""
|
||||
)
|
||||
fun getTransformationHistory(monId: Long): Flow<List<CharacterDtos.TransformationHistory>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
t.stageId as stageId,
|
||||
c.charaIndex AS monIndex,
|
||||
ca.name as cardName,
|
||||
t.transformationDate AS transformationDate
|
||||
FROM TransformationHistory t
|
||||
JOIN CardCharacter c ON c.id = t.stageId
|
||||
JOIN Card ca ON ca.id = c.cardId
|
||||
WHERE t.monId = :monId
|
||||
ORDER BY t.transformationDate ASC, t.id ASC
|
||||
"""
|
||||
)
|
||||
suspend fun getTransformationHistoryForExport(monId: Long): List<CharacterDtos.TransformationHistoryExport>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -48,21 +79,24 @@ interface UserCharacterDao {
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
c.name as nameSprite,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.spriteIdle2 AS spriteIdle2,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite as nameSprite,
|
||||
c.nameWidth as nameSpriteWidth,
|
||||
c.nameHeight as nameSpriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.characterId = uc.id as isInAdventure
|
||||
a.characterId = uc.id as isInAdventure,
|
||||
uc.isActive as active
|
||||
FROM UserCharacter uc
|
||||
JOIN Character c ON uc.charId = c.id
|
||||
JOIN Card d ON c.dimId = d.id
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Card d ON d.id = c.cardId
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Adventure a ON a.characterId = uc.id
|
||||
"""
|
||||
)
|
||||
suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites>
|
||||
fun getAllCharacters(): Flow<List<CharacterDtos.CharacterWithSprites>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -70,27 +104,40 @@ interface UserCharacterDao {
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
c.name as nameSprite,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.spriteIdle2 AS spriteIdle2,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite as nameSprite,
|
||||
c.nameWidth as nameSpriteWidth,
|
||||
c.nameHeight as nameSpriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.characterId = uc.id as isInAdventure
|
||||
a.characterId = uc.id as isInAdventure,
|
||||
uc.isActive as active
|
||||
FROM UserCharacter uc
|
||||
JOIN Character c ON uc.charId = c.id
|
||||
JOIN Card d ON c.dimId = d.id
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Card d ON c.cardId = d.id
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Adventure a ON a.characterId = uc.id
|
||||
WHERE uc.id = :id
|
||||
""")
|
||||
"""
|
||||
)
|
||||
suspend fun getCharacterWithSprites(id: Long): CharacterDtos.CharacterWithSprites
|
||||
|
||||
@Query("SELECT * FROM UserCharacter WHERE id = :id")
|
||||
suspend fun getCharacter(id: Long): UserCharacter
|
||||
|
||||
@Query("SELECT * FROM BECharacterData WHERE id = :id")
|
||||
suspend fun getBeData(id: Long): BECharacterData
|
||||
fun getBeData(id: Long): Flow<BECharacterData>
|
||||
|
||||
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
|
||||
fun getVbData(id: Long): Flow<VBCharacterData>
|
||||
|
||||
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
|
||||
suspend fun getVbDataOrNull(id: Long): VBCharacterData?
|
||||
|
||||
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
|
||||
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -98,22 +145,26 @@ interface UserCharacterDao {
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
c.sprite1 AS spriteIdle,
|
||||
c.spritesWidth AS spriteWidth,
|
||||
c.spritesHeight AS spriteHeight,
|
||||
c.name as nameSprite,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.spriteIdle2 AS spriteIdle2,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite as nameSprite,
|
||||
c.nameWidth as nameSpriteWidth,
|
||||
c.nameHeight as nameSpriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.characterId as isInAdventure
|
||||
a.characterId as isInAdventure,
|
||||
uc.isActive as active
|
||||
FROM UserCharacter uc
|
||||
JOIN Character c ON uc.charId = c.id
|
||||
JOIN Card d ON c.dimId = d.id
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Card d ON c.cardId = d.id
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Adventure a ON a.characterId = uc.id
|
||||
WHERE uc.isActive = 1
|
||||
LIMIT 1
|
||||
""")
|
||||
suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites?
|
||||
"""
|
||||
)
|
||||
fun getActiveCharacter(): Flow<CharacterDtos.CharacterWithSprites?>
|
||||
|
||||
@Query("DELETE FROM UserCharacter WHERE id = :id")
|
||||
fun deleteCharacterById(id: Long)
|
||||
@@ -127,11 +178,81 @@ interface UserCharacterDao {
|
||||
@Query(
|
||||
"""
|
||||
SELECT c.*
|
||||
FROM Character c
|
||||
FROM CardCharacter c
|
||||
join UserCharacter uc on c.id = uc.charId
|
||||
where uc.id = :charId
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun getCharacterInfo(charId: Long): Character
|
||||
suspend fun getCharacterInfo(charId: Long): CardCharacter
|
||||
|
||||
|
||||
@Query(
|
||||
"""
|
||||
INSERT INTO TransformationHistory(monId, stageId, transformationDate)
|
||||
VALUES
|
||||
(:monId,
|
||||
(SELECT id FROM CardCharacter WHERE charaIndex = :stage AND cardId = :dimId),
|
||||
:transformationDate)
|
||||
"""
|
||||
)
|
||||
fun insertTransformation(monId: Long, stage: Int, dimId: Long, transformationDate: Long)
|
||||
|
||||
@Upsert
|
||||
fun insertVitals(vararg vitalsHistory: VitalsHistory)
|
||||
|
||||
@Query("""SELECT * FROM VitalsHistory WHERE charId = :charId ORDER BY id ASC""")
|
||||
suspend fun getVitalsHistory(charId: Long): List<VitalsHistory>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.spriteIdle2 AS spriteIdle2,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite as nameSprite,
|
||||
c.nameWidth as nameSpriteWidth,
|
||||
c.nameHeight as nameSpriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.characterId = uc.id as isInAdventure,
|
||||
uc.isActive as active
|
||||
FROM UserCharacter uc
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Card d ON d.id = c.cardId
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Adventure a ON a.characterId = uc.id
|
||||
WHERE uc.characterType = "BEDevice"
|
||||
"""
|
||||
)
|
||||
suspend fun getBECharacters(): List<CharacterDtos.CharacterWithSprites>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
uc.*,
|
||||
c.stage,
|
||||
c.attribute,
|
||||
s.spriteIdle1 AS spriteIdle,
|
||||
s.spriteIdle2 AS spriteIdle2,
|
||||
s.width AS spriteWidth,
|
||||
s.height AS spriteHeight,
|
||||
c.nameSprite as nameSprite,
|
||||
c.nameWidth as nameSpriteWidth,
|
||||
c.nameHeight as nameSpriteHeight,
|
||||
d.isBEm as isBemCard,
|
||||
a.characterId = uc.id as isInAdventure,
|
||||
uc.isActive as active
|
||||
FROM UserCharacter uc
|
||||
JOIN CardCharacter c ON uc.charId = c.id
|
||||
JOIN Card d ON d.id = c.cardId
|
||||
JOIN Sprite s ON s.id = c.spriteId
|
||||
LEFT JOIN Adventure a ON a.characterId = uc.id
|
||||
WHERE uc.characterType = "VBDevice"
|
||||
"""
|
||||
)
|
||||
suspend fun getVBDimCharacters(): List<CharacterDtos.CharacterWithSprites>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.github.nacabaro.vbhelper.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
|
||||
|
||||
@Dao
|
||||
interface VitalWearSettingsDao {
|
||||
@Query("SELECT * FROM VitalWearCharacterSettings WHERE characterId = :characterId LIMIT 1")
|
||||
suspend fun getByCharacterId(characterId: Long): VitalWearCharacterSettings?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(settings: VitalWearCharacterSettings)
|
||||
}
|
||||
|
||||
@@ -2,41 +2,110 @@ package com.github.nacabaro.vbhelper.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.nacabaro.vbhelper.daos.AdventureDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardAdventureDao
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterDao
|
||||
import com.github.nacabaro.vbhelper.daos.DexDao
|
||||
import com.github.nacabaro.vbhelper.daos.DiMDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardFusionsDao
|
||||
import com.github.nacabaro.vbhelper.daos.CardProgressDao
|
||||
import com.github.nacabaro.vbhelper.daos.ItemDao
|
||||
import com.github.nacabaro.vbhelper.daos.SpecialMissionDao
|
||||
import com.github.nacabaro.vbhelper.daos.SpriteDao
|
||||
import com.github.nacabaro.vbhelper.daos.UserCharacterDao
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
import com.github.nacabaro.vbhelper.domain.Sprites
|
||||
import com.github.nacabaro.vbhelper.daos.VitalWearSettingsDao
|
||||
import com.github.nacabaro.vbhelper.daos.CharacterTransferPolicyDao
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardDao
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardEntity
|
||||
import com.github.nacabaro.vbhelper.domain.card.Background
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.card.Card
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardAdventure
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardFusions
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardProgress
|
||||
import com.github.nacabaro.vbhelper.domain.card.PossibleTransformations
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Sprite
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Adventure
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Dex
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalsHistory
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.VitalWearCharacterSettings
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.CharacterTransferPolicy
|
||||
import com.github.nacabaro.vbhelper.domain.items.Items
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
version = 3,
|
||||
exportSchema = false,
|
||||
entities = [
|
||||
Card::class,
|
||||
Character::class,
|
||||
Sprites::class,
|
||||
CardProgress::class,
|
||||
CardCharacter::class,
|
||||
CardAdventure::class,
|
||||
CardFusions::class,
|
||||
Sprite::class,
|
||||
UserCharacter::class,
|
||||
BECharacterData::class,
|
||||
VBCharacterData::class,
|
||||
SpecialMissions::class,
|
||||
TransformationHistory::class,
|
||||
VitalsHistory::class,
|
||||
Dex::class,
|
||||
Items::class,
|
||||
Adventure::class
|
||||
Adventure::class,
|
||||
Background::class,
|
||||
PossibleTransformations::class,
|
||||
ValidatedCardEntity::class,
|
||||
VitalWearCharacterSettings::class,
|
||||
CharacterTransferPolicy::class
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun dimDao(): DiMDao
|
||||
abstract fun cardDao(): CardDao
|
||||
abstract fun cardProgressDao(): CardProgressDao
|
||||
abstract fun characterDao(): CharacterDao
|
||||
abstract fun userCharacterDao(): UserCharacterDao
|
||||
abstract fun dexDao(): DexDao
|
||||
abstract fun itemDao(): ItemDao
|
||||
abstract fun adventureDao(): AdventureDao
|
||||
abstract fun spriteDao(): SpriteDao
|
||||
abstract fun specialMissionDao(): SpecialMissionDao
|
||||
abstract fun cardAdventureDao(): CardAdventureDao
|
||||
abstract fun cardFusionsDao(): CardFusionsDao
|
||||
abstract fun validatedCardDao(): ValidatedCardDao
|
||||
abstract fun vitalWearSettingsDao(): VitalWearSettingsDao
|
||||
abstract fun characterTransferPolicyDao(): CharacterTransferPolicyDao
|
||||
|
||||
companion object {
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// Create new tables
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Items` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `itemIcon` INTEGER NOT NULL, `itemLength` INTEGER NOT NULL, `price` INTEGER NOT NULL, `quantity` INTEGER NOT NULL, `itemType` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SpecialMissions` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `characterId` INTEGER NOT NULL, `goal` INTEGER NOT NULL, `watchId` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `status` TEXT NOT NULL, `timeElapsedInMinutes` INTEGER NOT NULL, `timeLimitInMinutes` INTEGER NOT NULL, `missionType` TEXT NOT NULL, FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `VBCharacterData` (`id` INTEGER NOT NULL, `generation` INTEGER NOT NULL, `totalTrophies` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `TransformationHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monId` INTEGER NOT NULL, `stageId` INTEGER NOT NULL, `transformationDate` INTEGER NOT NULL, FOREIGN KEY(`monId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stageId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `VitalsHistory` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charId` INTEGER NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `vitalPoints` INTEGER NOT NULL, FOREIGN KEY(`charId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Dex` (`id` INTEGER NOT NULL, `discoveredOn` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Adventure` (`characterId` INTEGER NOT NULL, `originalDuration` INTEGER NOT NULL, `finishesAdventure` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Background` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cardId` INTEGER NOT NULL, `background` BLOB NOT NULL, `backgroundWidth` INTEGER NOT NULL, `backgroundHeight` INTEGER NOT NULL, FOREIGN KEY(`cardId`) REFERENCES `Card`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `PossibleTransformations` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `charaId` INTEGER NOT NULL, `requiredVitals` INTEGER NOT NULL, `requiredTrophies` INTEGER NOT NULL, `requiredBattles` INTEGER NOT NULL, `requiredWinRate` INTEGER NOT NULL, `changeTimerHours` INTEGER NOT NULL, `requiredAdventureLevelCompleted` INTEGER NOT NULL, `toCharaId` INTEGER, FOREIGN KEY(`charaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`toCharaId`) REFERENCES `CardCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `validated` (`cardId` INTEGER NOT NULL, PRIMARY KEY(`cardId`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `VitalWearCharacterSettings` (`characterId` INTEGER NOT NULL, `trainingInBackground` INTEGER NOT NULL, `allowedBattles` INTEGER NOT NULL, `accumulatedDailyInjuries` INTEGER NOT NULL, PRIMARY KEY(`characterId`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `CharacterTransferPolicy` (`characterId` INTEGER NOT NULL, `nativeDeviceType` TEXT NOT NULL, `preferredHceExportFormat` TEXT NOT NULL, `preferredNfcaExportFormat` TEXT NOT NULL, `lastObservedImportFormat` TEXT, `lastTransferTransport` TEXT, `lastTransferTarget` TEXT, `preserveVbRoundTrip` INTEGER NOT NULL, `preserveBeRoundTrip` INTEGER NOT NULL, PRIMARY KEY(`characterId`), FOREIGN KEY(`characterId`) REFERENCES `UserCharacter`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
|
||||
// Add columns to existing tables
|
||||
try {
|
||||
db.execSQL("ALTER TABLE `UserCharacter` ADD COLUMN `characterType` TEXT NOT NULL DEFAULT 'BEDevice'")
|
||||
} catch (e: Exception) {}
|
||||
try {
|
||||
db.execSQL("ALTER TABLE `Card` ADD COLUMN `isBEm` INTEGER NOT NULL DEFAULT 0")
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.github.nacabaro.vbhelper.di
|
||||
|
||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||
import com.github.nacabaro.vbhelper.source.CurrencyRepository
|
||||
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
|
||||
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
|
||||
|
||||
interface AppContainer {
|
||||
val db: AppDatabase
|
||||
val dataStoreSecretsRepository: DataStoreSecretsRepository
|
||||
val currencyRepository: CurrencyRepository
|
||||
val validatedCardManager: ValidatedCardManager
|
||||
val companionLogService: CompanionLogService
|
||||
}
|
||||
@@ -1,20 +1,29 @@
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.dataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.room.Room
|
||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||
import com.github.nacabaro.vbhelper.di.AppContainer
|
||||
import com.github.nacabaro.vbhelper.source.CurrencyRepository
|
||||
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
|
||||
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
|
||||
import com.github.nacabaro.vbhelper.source.SecretsSerializer
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
|
||||
private const val SECRETS_DATA_STORE_NAME = "secrets.pb"
|
||||
|
||||
private const val USER_PREFERENCES_NAME = "user_preferences"
|
||||
val Context.secretsStore: DataStore<Secrets> by dataStore(
|
||||
fileName = SECRETS_DATA_STORE_NAME,
|
||||
serializer = SecretsSerializer
|
||||
)
|
||||
|
||||
val Context.currencyStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = USER_PREFERENCES_NAME
|
||||
)
|
||||
|
||||
class DefaultAppContainer(private val context: Context) : AppContainer {
|
||||
|
||||
override val db: AppDatabase by lazy {
|
||||
@@ -23,11 +32,19 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
|
||||
klass = AppDatabase::class.java,
|
||||
"internalDb"
|
||||
)
|
||||
.addMigrations(AppDatabase.MIGRATION_1_2)
|
||||
.createFromAsset("items.db")
|
||||
.build()
|
||||
}
|
||||
|
||||
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
|
||||
|
||||
override val currencyRepository = CurrencyRepository(context.currencyStore)
|
||||
|
||||
override val validatedCardManager by lazy {
|
||||
ValidatedCardManager(db.validatedCardDao())
|
||||
}
|
||||
|
||||
override val companionLogService = CompanionLogService()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,18 @@ package com.github.nacabaro.vbhelper.di
|
||||
|
||||
import DefaultAppContainer
|
||||
import android.app.Application
|
||||
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
|
||||
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
|
||||
|
||||
class VBHelper : Application() {
|
||||
lateinit var container: DefaultAppContainer
|
||||
|
||||
val validatedCardManager: ValidatedCardManager
|
||||
get() = container.validatedCardManager
|
||||
|
||||
val companionLogService: CompanionLogService
|
||||
get() = container.companionLogService
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
container = DefaultAppContainer(applicationContext)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.domain
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Card
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = User::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["userId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["dimId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class DimProgress(
|
||||
@PrimaryKey val dimId: Int,
|
||||
@PrimaryKey val userId: Int,
|
||||
val currentStage: Int,
|
||||
val unlocked: Boolean
|
||||
)
|
||||
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.domain
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.domain
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Sprites(
|
||||
@PrimaryKey(autoGenerate = true) val id : Int,
|
||||
val sprite: ByteArray,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
)
|
||||
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.domain
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["cardId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index(value = ["cardId"])]
|
||||
)
|
||||
data class Background (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||
val cardId: Long,
|
||||
val background: ByteArray,
|
||||
val backgroundWidth: Int,
|
||||
val backgroundHeight: Int
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Background
|
||||
|
||||
if (id != other.id) return false
|
||||
if (cardId != other.cardId) return false
|
||||
if (!background.contentEquals(other.background)) return false
|
||||
if (backgroundWidth != other.backgroundWidth) return false
|
||||
if (backgroundHeight != other.backgroundHeight) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + cardId.hashCode()
|
||||
result = 31 * result + background.contentHashCode()
|
||||
result = 31 * result + backgroundWidth
|
||||
result = 31 * result + backgroundHeight
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Card(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val cardId: Int,
|
||||
val logo: ByteArray,
|
||||
val logoWidth: Int,
|
||||
val logoHeight: Int,
|
||||
val name: String,
|
||||
val stageCount: Int,
|
||||
val isBEm: Boolean
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Card
|
||||
|
||||
if (id != other.id) return false
|
||||
if (cardId != other.cardId) return false
|
||||
if (!logo.contentEquals(other.logo)) return false
|
||||
if (logoWidth != other.logoWidth) return false
|
||||
if (logoHeight != other.logoHeight) return false
|
||||
if (name != other.name) return false
|
||||
if (stageCount != other.stageCount) return false
|
||||
if (isBEm != other.isBEm) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + cardId
|
||||
result = 31 * result + logo.contentHashCode()
|
||||
result = 31 * result + logoWidth
|
||||
result = 31 * result + logoHeight
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + stageCount
|
||||
result = 31 * result + isBEm.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["characterId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["cardId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["characterId"]),
|
||||
Index(value = ["cardId"])
|
||||
]
|
||||
)
|
||||
data class CardAdventure(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val cardId: Long,
|
||||
val characterId: Long,
|
||||
val steps: Int,
|
||||
val bossHp: Int,
|
||||
val bossAp: Int,
|
||||
val bossDp: Int,
|
||||
val bossBp: Int?
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Sprite
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["cardId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Sprite::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["spriteId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["cardId"]),
|
||||
Index(value = ["spriteId"])
|
||||
]
|
||||
)
|
||||
|
||||
/*
|
||||
* Character represents a character on a card. There should only be one of these per dimId
|
||||
* and monIndex.
|
||||
* TODO: Customs will mean this should be unique per cardName and monIndex
|
||||
*/
|
||||
data class CardCharacter (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val cardId: Long,
|
||||
val spriteId: Long,
|
||||
val charaIndex: Int,
|
||||
val stage: Int, // These should be replaced with enums
|
||||
val attribute: NfcCharacter.Attribute, // This one too
|
||||
val baseHp: Int,
|
||||
val baseBp: Int,
|
||||
val baseAp: Int,
|
||||
val nameSprite: ByteArray,
|
||||
val nameWidth: Int,
|
||||
val nameHeight: Int
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardCharacter
|
||||
|
||||
if (id != other.id) return false
|
||||
if (cardId != other.cardId) return false
|
||||
if (spriteId != other.spriteId) return false
|
||||
if (charaIndex != other.charaIndex) return false
|
||||
if (stage != other.stage) return false
|
||||
if (attribute != other.attribute) return false
|
||||
if (baseHp != other.baseHp) return false
|
||||
if (baseBp != other.baseBp) return false
|
||||
if (baseAp != other.baseAp) return false
|
||||
if (!nameSprite.contentEquals(other.nameSprite)) return false
|
||||
if (nameWidth != other.nameWidth) return false
|
||||
if (nameHeight != other.nameHeight) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + cardId.hashCode()
|
||||
result = 31 * result + spriteId.hashCode()
|
||||
result = 31 * result + charaIndex
|
||||
result = 31 * result + stage
|
||||
result = 31 * result + attribute.hashCode()
|
||||
result = 31 * result + baseHp
|
||||
result = 31 * result + baseBp
|
||||
result = 31 * result + baseAp
|
||||
result = 31 * result + nameSprite.contentHashCode()
|
||||
result = 31 * result + nameWidth
|
||||
result = 31 * result + nameHeight
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["fromCharaId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["toCharaId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["fromCharaId"]),
|
||||
Index(value = ["toCharaId"])
|
||||
]
|
||||
)
|
||||
data class CardFusions(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||
val fromCharaId: Long,
|
||||
val attribute: NfcCharacter.Attribute,
|
||||
val toCharaId: Long
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["cardId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CardProgress(
|
||||
@PrimaryKey val cardId: Long,
|
||||
val currentStage: Int,
|
||||
val unlocked: Boolean
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.github.nacabaro.vbhelper.domain.card
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["charaId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["toCharaId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["charaId"]),
|
||||
Index(value = ["toCharaId"])
|
||||
]
|
||||
)
|
||||
data class PossibleTransformations (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
var charaId: Long,
|
||||
val requiredVitals: Int,
|
||||
val requiredTrophies: Int,
|
||||
val requiredBattles: Int,
|
||||
val requiredWinRate: Int,
|
||||
val changeTimerHours: Int,
|
||||
val requiredAdventureLevelCompleted: Int,
|
||||
val toCharaId: Long?
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Card(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val dimId: Int,
|
||||
val logo: ByteArray,
|
||||
val logoWidth: Int,
|
||||
val logoHeight: Int,
|
||||
val name: String,
|
||||
val stageCount: Int,
|
||||
val isBEm: Boolean
|
||||
)
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.ForeignKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Card::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["dimId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
/*
|
||||
* Character represents a character on a DIM card. There should only be one of these per dimId
|
||||
* and monIndex.
|
||||
* TODO: Customs will mean this should be unique per cardName and monIndex
|
||||
*/
|
||||
data class Character (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val dimId: Long,
|
||||
val monIndex: Int,
|
||||
val name: ByteArray,
|
||||
val stage: Int, // These should be replaced with enums
|
||||
val attribute: NfcCharacter.Attribute, // This one too
|
||||
val baseHp: Int,
|
||||
val baseBp: Int,
|
||||
val baseAp: Int,
|
||||
val sprite1: ByteArray,
|
||||
val sprite2: ByteArray,
|
||||
val nameWidth: Int,
|
||||
val nameHeight: Int,
|
||||
val spritesWidth: Int,
|
||||
val spritesHeight: Int
|
||||
)
|
||||
@@ -3,11 +3,12 @@ package com.github.nacabaro.vbhelper.domain.characters
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Character::class,
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.github.nacabaro.vbhelper.domain.characters
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class Sprite(
|
||||
@PrimaryKey(autoGenerate = true) val id : Int = 0,
|
||||
val spriteIdle1: ByteArray,
|
||||
val spriteIdle2: ByteArray,
|
||||
val spriteWalk1: ByteArray,
|
||||
val spriteWalk2: ByteArray,
|
||||
val spriteRun1: ByteArray,
|
||||
val spriteRun2: ByteArray,
|
||||
val spriteTrain1: ByteArray,
|
||||
val spriteTrain2: ByteArray,
|
||||
val spriteHappy: ByteArray,
|
||||
val spriteSleep: ByteArray,
|
||||
val spriteAttack: ByteArray,
|
||||
val spriteDodge: ByteArray,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Sprite
|
||||
|
||||
if (id != other.id) return false
|
||||
if (!spriteIdle1.contentEquals(other.spriteIdle1)) return false
|
||||
if (!spriteIdle2.contentEquals(other.spriteIdle2)) return false
|
||||
if (!spriteWalk1.contentEquals(other.spriteWalk1)) return false
|
||||
if (!spriteWalk2.contentEquals(other.spriteWalk2)) return false
|
||||
if (!spriteRun1.contentEquals(other.spriteRun1)) return false
|
||||
if (!spriteRun2.contentEquals(other.spriteRun2)) return false
|
||||
if (!spriteTrain1.contentEquals(other.spriteTrain1)) return false
|
||||
if (!spriteTrain2.contentEquals(other.spriteTrain2)) return false
|
||||
if (!spriteHappy.contentEquals(other.spriteHappy)) return false
|
||||
if (!spriteSleep.contentEquals(other.spriteSleep)) return false
|
||||
if (!spriteAttack.contentEquals(other.spriteAttack)) return false
|
||||
if (!spriteDodge.contentEquals(other.spriteDodge)) return false
|
||||
if (width != other.width) return false
|
||||
if (height != other.height) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id
|
||||
result = 31 * result + spriteIdle1.contentHashCode()
|
||||
result = 31 * result + spriteIdle2.contentHashCode()
|
||||
result = 31 * result + spriteWalk1.contentHashCode()
|
||||
result = 31 * result + spriteWalk2.contentHashCode()
|
||||
result = 31 * result + spriteRun1.contentHashCode()
|
||||
result = 31 * result + spriteRun2.contentHashCode()
|
||||
result = 31 * result + spriteTrain1.contentHashCode()
|
||||
result = 31 * result + spriteTrain2.contentHashCode()
|
||||
result = 31 * result + spriteHappy.contentHashCode()
|
||||
result = 31 * result + spriteSleep.contentHashCode()
|
||||
result = 31 * result + spriteAttack.contentHashCode()
|
||||
result = 31 * result + spriteDodge.contentHashCode()
|
||||
result = 31 * result + width
|
||||
result = 31 * result + height
|
||||
return result
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.transfer.ExportFormat
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTarget
|
||||
import com.github.nacabaro.vbhelper.transfer.TransferTransport
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = UserCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["characterId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CharacterTransferPolicy(
|
||||
@PrimaryKey val characterId: Long,
|
||||
val nativeDeviceType: DeviceType,
|
||||
val preferredHceExportFormat: ExportFormat,
|
||||
val preferredNfcaExportFormat: ExportFormat,
|
||||
val lastObservedImportFormat: ExportFormat?,
|
||||
val lastTransferTransport: TransferTransport?,
|
||||
val lastTransferTarget: TransferTarget?,
|
||||
val preserveVbRoundTrip: Boolean,
|
||||
val preserveBeRoundTrip: Boolean,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.vb.SpecialMission
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = UserCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["characterId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index(value = ["characterId"])]
|
||||
)
|
||||
data class SpecialMissions (
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||
var characterId: Long,
|
||||
var goal: Int,
|
||||
val watchId: Int,
|
||||
val progress: Int,
|
||||
val status: SpecialMission.Status,
|
||||
val timeElapsedInMinutes: Int,
|
||||
val timeLimitInMinutes: Int,
|
||||
val missionType: SpecialMission.Type
|
||||
)
|
||||
+7
-2
@@ -1,9 +1,10 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
@@ -14,11 +15,15 @@ import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Character::class,
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["stageId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["monId"]),
|
||||
Index(value = ["stageId"])
|
||||
]
|
||||
)
|
||||
data class TransformationHistory (
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
|
||||
/**
|
||||
* UnifiedCharacter provides a single access point for common character data
|
||||
* and bridges the specific data for VB and BE characters.
|
||||
*/
|
||||
data class UnifiedCharacter(
|
||||
val userCharacter: UserCharacter,
|
||||
val vbData: VBCharacterData? = null,
|
||||
val beData: BECharacterData? = null
|
||||
) {
|
||||
val characterType: DeviceType get() = userCharacter.characterType
|
||||
|
||||
fun isVB() = characterType == DeviceType.VBDevice
|
||||
fun isBE() = characterType == DeviceType.BEDevice
|
||||
|
||||
// Unified accessors for core stats
|
||||
val trophies: Int get() = userCharacter.trophies
|
||||
val ageInDays: Int get() = userCharacter.ageInDays
|
||||
val mood: Int get() = userCharacter.mood
|
||||
val vitalPoints: Int get() = userCharacter.vitalPoints
|
||||
val injuryStatus = userCharacter.injuryStatus
|
||||
|
||||
// BE specific accessors with defaults
|
||||
val trainingHp: Int get() = beData?.trainingHp ?: 0
|
||||
val trainingAp: Int get() = beData?.trainingAp ?: 0
|
||||
val trainingBp: Int get() = beData?.trainingBp ?: 0
|
||||
val abilityRarity = beData?.abilityRarity
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.utils.DeviceType
|
||||
import com.github.nacabaro.vbhelper.domain.characters.Character
|
||||
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Character::class,
|
||||
entity = CardCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["charId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
],
|
||||
indices = [Index(value = ["charId"])]
|
||||
)
|
||||
/**
|
||||
* UserCharacter represents and instance of a character. The charId should map to a Character
|
||||
@@ -24,7 +26,6 @@ data class UserCharacter (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
var charId: Long,
|
||||
var ageInDays: Int,
|
||||
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
|
||||
var mood: Int,
|
||||
var vitalPoints: Int,
|
||||
var transformationCountdown: Int,
|
||||
|
||||
+19
-2
@@ -1,4 +1,21 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
class VBCharacterData {
|
||||
}
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = UserCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class VBCharacterData (
|
||||
@PrimaryKey val id: Long,
|
||||
val generation: Int,
|
||||
val totalTrophies: Int
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class VitalWearCharacterSettings(
|
||||
@PrimaryKey val characterId: Long,
|
||||
val trainingInBackground: Boolean = false,
|
||||
val allowedBattles: Int = 1,
|
||||
val accumulatedDailyInjuries: Int = 0,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.github.nacabaro.vbhelper.domain.device_data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = UserCharacter::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["charId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index(value = ["charId"])]
|
||||
)
|
||||
data class VitalsHistory (
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val charId: Long,
|
||||
val year: Int,
|
||||
val month: Int,
|
||||
val day: Int,
|
||||
val vitalPoints: Int
|
||||
)
|
||||
@@ -3,6 +3,13 @@ package com.github.nacabaro.vbhelper.domain.items
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
enum class ItemType {
|
||||
VBITEM,
|
||||
BEITEM,
|
||||
UNIVERSAL,
|
||||
SPECIALMISSION
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class Items(
|
||||
@PrimaryKey val id: Long,
|
||||
@@ -11,5 +18,6 @@ data class Items(
|
||||
val itemIcon: Int,
|
||||
val itemLength: Int,
|
||||
val price: Int,
|
||||
val quantity: Int
|
||||
val quantity: Int,
|
||||
val itemType: ItemType
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.nacabaro.vbhelper.domain.transfer
|
||||
|
||||
import com.github.cfogrady.vitalwear.common.data.SharedTransferSeenEntity
|
||||
|
||||
@Deprecated("Use SharedTransferSeenEntity from the shared transfer database.")
|
||||
typealias TransferSeenEntity = SharedTransferSeenEntity
|
||||
|
||||
@@ -9,5 +9,109 @@ object CardDtos {
|
||||
val logoHeight: Int,
|
||||
val totalCharacters: Int,
|
||||
val obtainedCharacters: Int,
|
||||
)
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardProgress
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (cardName != other.cardName) return false
|
||||
if (!cardLogo.contentEquals(other.cardLogo)) return false
|
||||
if (logoWidth != other.logoWidth) return false
|
||||
if (logoHeight != other.logoHeight) return false
|
||||
if (totalCharacters != other.totalCharacters) return false
|
||||
if (obtainedCharacters != other.obtainedCharacters) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = cardId.hashCode()
|
||||
result = 31 * result + cardName.hashCode()
|
||||
result = 31 * result + cardLogo.contentHashCode()
|
||||
result = 31 * result + logoWidth
|
||||
result = 31 * result + logoHeight
|
||||
result = 31 * result + totalCharacters
|
||||
result = 31 * result + obtainedCharacters
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class CardAdventureWithSprites (
|
||||
val characterName: ByteArray,
|
||||
val characterNameWidth: Int,
|
||||
val characterNameHeight: Int,
|
||||
val characterIdleSprite: ByteArray,
|
||||
val characterIdleSpriteWidth: Int,
|
||||
val characterIdleSpriteHeight: Int,
|
||||
val characterAp: Int,
|
||||
val characterBp: Int?,
|
||||
val characterDp: Int,
|
||||
val characterHp: Int,
|
||||
val steps: Int,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardAdventureWithSprites
|
||||
|
||||
if (!characterName.contentEquals(other.characterName)) return false
|
||||
if (characterNameWidth != other.characterNameWidth) return false
|
||||
if (characterNameHeight != other.characterNameHeight) return false
|
||||
if (!characterIdleSprite.contentEquals(other.characterIdleSprite)) return false
|
||||
if (characterIdleSpriteWidth != other.characterIdleSpriteWidth) return false
|
||||
if (characterIdleSpriteHeight != other.characterIdleSpriteHeight) return false
|
||||
if (characterAp != other.characterAp) return false
|
||||
if (characterBp != other.characterBp) return false
|
||||
if (characterDp != other.characterDp) return false
|
||||
if (characterHp != other.characterHp) return false
|
||||
if (steps != other.steps) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = characterName.contentHashCode()
|
||||
result = 31 * result + characterNameWidth
|
||||
result = 31 * result + characterNameHeight
|
||||
result = 31 * result + characterIdleSprite.contentHashCode()
|
||||
result = 31 * result + characterIdleSpriteWidth
|
||||
result = 31 * result + characterIdleSpriteHeight
|
||||
result = 31 * result + characterAp
|
||||
result = 31 * result + (characterBp ?: 0)
|
||||
result = 31 * result + characterDp
|
||||
result = 31 * result + characterHp
|
||||
result = 31 * result + steps
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class CardIcon (
|
||||
val cardIcon: ByteArray,
|
||||
val cardIconWidth: Int,
|
||||
val cardIconHeight: Int
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardIcon
|
||||
|
||||
if (!cardIcon.contentEquals(other.cardIcon)) return false
|
||||
if (cardIconWidth != other.cardIconWidth) return false
|
||||
if (cardIconHeight != other.cardIconHeight) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = cardIcon.contentHashCode()
|
||||
result = 31 * result + cardIconWidth
|
||||
result = 31 * result + cardIconHeight
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ object CharacterDtos {
|
||||
var stage: Int,
|
||||
var attribute: NfcCharacter.Attribute,
|
||||
var ageInDays: Int,
|
||||
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
|
||||
var mood: Int,
|
||||
var vitalPoints: Int,
|
||||
var transformationCountdown: Int,
|
||||
@@ -25,18 +24,91 @@ object CharacterDtos {
|
||||
var heartRateCurrent: Int,
|
||||
var characterType: DeviceType,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteIdle2: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val nameSprite: ByteArray,
|
||||
val nameSpriteWidth: Int,
|
||||
val nameSpriteHeight: Int,
|
||||
val isBemCard: Boolean,
|
||||
val isInAdventure: Boolean
|
||||
)
|
||||
val isInAdventure: Boolean,
|
||||
val active: Boolean
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
data class DiMInfo(
|
||||
val cardId: Int,
|
||||
val charId: Int
|
||||
other as CharacterWithSprites
|
||||
|
||||
if (id != other.id) return false
|
||||
if (charId != other.charId) return false
|
||||
if (stage != other.stage) return false
|
||||
if (attribute != other.attribute) return false
|
||||
if (ageInDays != other.ageInDays) return false
|
||||
if (mood != other.mood) return false
|
||||
if (vitalPoints != other.vitalPoints) return false
|
||||
if (transformationCountdown != other.transformationCountdown) return false
|
||||
if (injuryStatus != other.injuryStatus) return false
|
||||
if (trophies != other.trophies) return false
|
||||
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
|
||||
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
|
||||
if (totalBattlesWon != other.totalBattlesWon) return false
|
||||
if (totalBattlesLost != other.totalBattlesLost) return false
|
||||
if (activityLevel != other.activityLevel) return false
|
||||
if (heartRateCurrent != other.heartRateCurrent) return false
|
||||
if (characterType != other.characterType) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (!spriteIdle2.contentEquals(other.spriteIdle2)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (!nameSprite.contentEquals(other.nameSprite)) return false
|
||||
if (nameSpriteWidth != other.nameSpriteWidth) return false
|
||||
if (nameSpriteHeight != other.nameSpriteHeight) return false
|
||||
if (isBemCard != other.isBemCard) return false
|
||||
if (isInAdventure != other.isInAdventure) return false
|
||||
if (active != other.active) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + charId.hashCode()
|
||||
result = 31 * result + stage
|
||||
result = 31 * result + attribute.hashCode()
|
||||
result = 31 * result + ageInDays
|
||||
result = 31 * result + mood
|
||||
result = 31 * result + vitalPoints
|
||||
result = 31 * result + transformationCountdown
|
||||
result = 31 * result + injuryStatus.hashCode()
|
||||
result = 31 * result + trophies
|
||||
result = 31 * result + currentPhaseBattlesWon
|
||||
result = 31 * result + currentPhaseBattlesLost
|
||||
result = 31 * result + totalBattlesWon
|
||||
result = 31 * result + totalBattlesLost
|
||||
result = 31 * result + activityLevel
|
||||
result = 31 * result + heartRateCurrent
|
||||
result = 31 * result + characterType.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteIdle2.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + nameSprite.contentHashCode()
|
||||
result = 31 * result + nameSpriteWidth
|
||||
result = 31 * result + nameSpriteHeight
|
||||
result = 31 * result + isBemCard.hashCode()
|
||||
result = 31 * result + isInAdventure.hashCode()
|
||||
result = 31 * result + active.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class CardCharacterInfo(
|
||||
val cardId: Long,
|
||||
val charId: Int,
|
||||
val stage: Int,
|
||||
val attribute: NfcCharacter.Attribute,
|
||||
val currentStage: Int
|
||||
)
|
||||
|
||||
data class TransformationHistory(
|
||||
@@ -46,15 +118,125 @@ object CharacterDtos {
|
||||
val spriteHeight: Int,
|
||||
val monIndex: Int,
|
||||
val transformationDate: Long
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TransformationHistory
|
||||
|
||||
if (id != other.id) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (monIndex != other.monIndex) return false
|
||||
if (transformationDate != other.transformationDate) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + monIndex
|
||||
result = 31 * result + transformationDate.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class TransformationHistoryExport(
|
||||
val stageId: Long,
|
||||
val monIndex: Int,
|
||||
val cardName: String,
|
||||
val transformationDate: Long
|
||||
)
|
||||
|
||||
data class CardCharaProgress(
|
||||
val id: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val nameSprite: ByteArray,
|
||||
val nameSpriteWidth: Int,
|
||||
val nameSpriteHeight: Int,
|
||||
val discoveredOn: Long?,
|
||||
val baseHp: Int,
|
||||
val baseBp: Int,
|
||||
val baseAp: Int,
|
||||
val stage: Int,
|
||||
val attribute: NfcCharacter.Attribute,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardCharaProgress
|
||||
|
||||
if (id != other.id) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (!nameSprite.contentEquals(other.nameSprite)) return false
|
||||
if (nameSpriteWidth != other.nameSpriteWidth) return false
|
||||
if (nameSpriteHeight != other.nameSpriteHeight) return false
|
||||
if (discoveredOn != other.discoveredOn) return false
|
||||
if (baseHp != other.baseHp) return false
|
||||
if (baseBp != other.baseBp) return false
|
||||
if (baseAp != other.baseAp) return false
|
||||
if (stage != other.stage) return false
|
||||
if (attribute != other.attribute) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + nameSprite.contentHashCode()
|
||||
result = 31 * result + nameSpriteWidth
|
||||
result = 31 * result + nameSpriteHeight
|
||||
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
|
||||
result = 31 * result + baseHp
|
||||
result = 31 * result + baseBp
|
||||
result = 31 * result + baseAp
|
||||
result = 31 * result + stage
|
||||
result = 31 * result + attribute.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class CardProgress(
|
||||
val id: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val discoveredOn: Long?
|
||||
)
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CardProgress
|
||||
|
||||
if (id != other.id) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class AdventureCharacterWithSprites(
|
||||
var id: Long = 0,
|
||||
@@ -62,7 +244,6 @@ object CharacterDtos {
|
||||
var stage: Int,
|
||||
var attribute: NfcCharacter.Attribute,
|
||||
var ageInDays: Int,
|
||||
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
|
||||
var mood: Int,
|
||||
var vitalPoints: Int,
|
||||
var transformationCountdown: Int,
|
||||
@@ -81,5 +262,156 @@ object CharacterDtos {
|
||||
val isBemCard: Boolean,
|
||||
val finishesAdventure: Long,
|
||||
val originalTimeInMinutes: Long
|
||||
)
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as AdventureCharacterWithSprites
|
||||
|
||||
if (id != other.id) return false
|
||||
if (charId != other.charId) return false
|
||||
if (stage != other.stage) return false
|
||||
if (attribute != other.attribute) return false
|
||||
if (ageInDays != other.ageInDays) return false
|
||||
if (mood != other.mood) return false
|
||||
if (vitalPoints != other.vitalPoints) return false
|
||||
if (transformationCountdown != other.transformationCountdown) return false
|
||||
if (injuryStatus != other.injuryStatus) return false
|
||||
if (trophies != other.trophies) return false
|
||||
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
|
||||
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
|
||||
if (totalBattlesWon != other.totalBattlesWon) return false
|
||||
if (totalBattlesLost != other.totalBattlesLost) return false
|
||||
if (activityLevel != other.activityLevel) return false
|
||||
if (heartRateCurrent != other.heartRateCurrent) return false
|
||||
if (characterType != other.characterType) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (isBemCard != other.isBemCard) return false
|
||||
if (finishesAdventure != other.finishesAdventure) return false
|
||||
if (originalTimeInMinutes != other.originalTimeInMinutes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + charId.hashCode()
|
||||
result = 31 * result + stage
|
||||
result = 31 * result + attribute.hashCode()
|
||||
result = 31 * result + ageInDays
|
||||
result = 31 * result + mood
|
||||
result = 31 * result + vitalPoints
|
||||
result = 31 * result + transformationCountdown
|
||||
result = 31 * result + injuryStatus.hashCode()
|
||||
result = 31 * result + trophies
|
||||
result = 31 * result + currentPhaseBattlesWon
|
||||
result = 31 * result + currentPhaseBattlesLost
|
||||
result = 31 * result + totalBattlesWon
|
||||
result = 31 * result + totalBattlesLost
|
||||
result = 31 * result + activityLevel
|
||||
result = 31 * result + heartRateCurrent
|
||||
result = 31 * result + characterType.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + isBemCard.hashCode()
|
||||
result = 31 * result + finishesAdventure.hashCode()
|
||||
result = 31 * result + originalTimeInMinutes.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class EvolutionRequirementsWithSpritesAndObtained(
|
||||
val charaId: Long,
|
||||
val fromCharaId: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val discoveredOn: Long?,
|
||||
val requiredTrophies: Int,
|
||||
val requiredVitals: Int,
|
||||
val requiredBattles: Int,
|
||||
val requiredWinRate: Int,
|
||||
val changeTimerHours: Int,
|
||||
val requiredAdventureLevelCompleted: Int
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as EvolutionRequirementsWithSpritesAndObtained
|
||||
|
||||
if (charaId != other.charaId) return false
|
||||
if (fromCharaId != other.fromCharaId) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (discoveredOn != other.discoveredOn) return false
|
||||
if (requiredTrophies != other.requiredTrophies) return false
|
||||
if (requiredVitals != other.requiredVitals) return false
|
||||
if (requiredBattles != other.requiredBattles) return false
|
||||
if (requiredWinRate != other.requiredWinRate) return false
|
||||
if (changeTimerHours != other.changeTimerHours) return false
|
||||
if (requiredAdventureLevelCompleted != other.requiredAdventureLevelCompleted) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = charaId.hashCode()
|
||||
result = 31 * result + fromCharaId.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
|
||||
result = 31 * result + requiredTrophies
|
||||
result = 31 * result + requiredVitals
|
||||
result = 31 * result + requiredBattles
|
||||
result = 31 * result + requiredWinRate
|
||||
result = 31 * result + changeTimerHours
|
||||
result = 31 * result + requiredAdventureLevelCompleted
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class FusionsWithSpritesAndObtained(
|
||||
val charaId: Long,
|
||||
val fromCharaId: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
val spriteWidth: Int,
|
||||
val spriteHeight: Int,
|
||||
val discoveredOn: Long?,
|
||||
val fusionAttribute: NfcCharacter.Attribute
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FusionsWithSpritesAndObtained
|
||||
|
||||
if (charaId != other.charaId) return false
|
||||
if (fromCharaId != other.fromCharaId) return false
|
||||
if (!spriteIdle.contentEquals(other.spriteIdle)) return false
|
||||
if (spriteWidth != other.spriteWidth) return false
|
||||
if (spriteHeight != other.spriteHeight) return false
|
||||
if (discoveredOn != other.discoveredOn) return false
|
||||
if (fusionAttribute != other.fusionAttribute) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = charaId.hashCode()
|
||||
result = 31 * result + fromCharaId.hashCode()
|
||||
result = 31 * result + spriteIdle.contentHashCode()
|
||||
result = 31 * result + spriteWidth
|
||||
result = 31 * result + spriteHeight
|
||||
result = 31 * result + (discoveredOn?.hashCode() ?: 0)
|
||||
result = 31 * result + fusionAttribute.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user