Compare commits

...
250 Commits
Author SHA1 Message Date
nacabaroandGitHub 99d309adc1 Merge pull request #60 from b13st/fix/hce-send-crash
Fix double character deletion causing crash on HCE transfer to VitalWear
2026-07-01 17:24:49 +02:00
nacabaroandGitHub 2760af5d2b Merge pull request #59 from b13st/chore/visual-improvements
improving various visuals
2026-07-01 17:23:34 +02:00
b13st 931f730406 Fix double character deletion causing crash on HCE transfer to VitalWear
Character was deleted from the local DB both right after sendCharacterToWatch
and again in WritingScreen's completion callback. The second deletion failed
because the character no longer existed, causing the reported random crash
when sending back to the watch. Deletion is now handled only once, in
WritingScreen.
2026-07-01 15:04:26 +02:00
b13st 4eaf6cdf1b Fix double character deletion causing crash on HCE transfer to VitalWear
Character was deleted from the local DB both right after sendCharacterToWatch
and again in WritingScreen's completion callback. The second deletion failed
because the character no longer existed, causing the reported random crash
when sending back to the watch. Deletion is now handled only once, in
WritingScreen.
2026-07-01 15:00:56 +02:00
b13st 416b30f7a8 improving various visuals 2026-07-01 13:51:34 +02:00
nacabaro df713855f5 I forgot to update the version number. 2026-06-29 21:26:11 +02:00
nacabaro e5ccff16d0 Pulidora industrial
- Removed all the warnings during building
- Upgraded all the libraries
- Upgraded grandle and the SDK version
- Fix japanese strings
- Added vibration permission
2026-06-29 16:15:12 +00:00
nacabaroandGitHub 4b866aad8a Merge pull request #57 from b13st/fix/special-missions-nan-crash
Fix special missions crash and NaN% stat display
2026-06-24 23:05:51 +02:00
nacabaroandGitHub 33ad2728b8 Merge pull request #56 from b13st/fix/hce-character-cloning
Fix/hce character cloning
2026-06-24 23:04:53 +02:00
b13st 05f04fdc49 Fix LaunchedEffect key so characterToNfc runs when active character loads
When launching the scan screen from the home button, characterId is
loaded asynchronously from the active character. The previous key
(storageRepository) never re-triggered the effect, so characterToNfc
(which sets pendingExportCharacterId) was skipped if characterId was
null on first composition, making the HCE delete unreachable.
2026-06-24 15:40:19 +02:00
b13st 07315fe11b Delete character from app after successful HCE transfer to VitalWear watch
When sending a character to the VitalWear watch via HCE, the transfer
is atomic (single NFC interaction). The character was being left in the
app after transfer, allowing it to be cloned indefinitely.

Delete the character from the database immediately after sendCharacterToWatch
succeeds, matching the behavior of physical bracelet transfers which delete
the character once both write phases complete.
2026-06-24 15:29:58 +02:00
b13st 2e72b543f6 Fix special missions crash and NaN% stat display
- Fix crash when tapping a completed special mission: onClickCollect
  was ignoring the missionId passed by SpecialMissionsEntry and using
  selectedSpecialMissionId (-1 by default), causing Room to query id=-1
  on a non-nullable Flow which throws when no row exists.
- Fix NaN% in current phase win rate: the guard was checking
  totalBattlesLost==0 but dividing by currentPhaseBattles, producing
  0/0=NaN when total>0 but current phase has no battles yet.
- Guard against empty items list in clearSpecialMission to avoid
  NoSuchElementException from random() on an empty collection.
2026-06-24 15:24:54 +02:00
nacabaroandGitHub 81bfec5ac5 Merge pull request #55 from b13st/fix/vitalwear-hce-transfer
Fix VitalWear HCE transfer in both directions
2026-06-23 20:30:46 +00:00
nacabaroandGitHub 8ec98b9d39 Merge pull request #54 from shiki8826/main
Thank you very much
2026-06-23 20:30:04 +00:00
b13st e549fb4469 Fix VitalWear HCE transfer in both directions
- Detect ISO-DEP tags (VitalWear watch) vs NFC-A (physical bracelets)
- Watch->Phone: use VitalWearHceReaderClient + VitalWearCharacterImporter
- Phone->Watch: use VitalWearHceReaderClient + VitalWearCharacterExporter
- Add AppDatabase to ScanScreenControllerImpl for HCE import/export
- Extract buildCharacterProto() from VitalWearCharacterExporter
- Add JitPack repository to settings.gradle.kts
2026-06-23 13:44:26 +02:00
shiki8826 8d62437aae 日本語翻訳の修正及び追加項目の翻訳 2026-06-19 20:46:23 +09:00
nacabaro 7a8815694d Remove test file, i'm not figuring this out tonight 2026-06-18 22:44:14 +00:00
nacabaro 464f2e48a7 Add basic migration to support the vitalwear thing 2026-06-18 22:37:51 +00:00
nacabaro f641e8aaa0 Compile fix before merging 2026-06-18 22:17:52 +00:00
nacabaroandGitHub c67dc4bd02 Merge pull request #52 from shiki8826/main
Japanese Translation
2026-06-13 00:51:40 +02:00
nacabaroandGitHub 8aa31e819b Merge pull request #53 from 457R0/main
This should be the correct one without problem-report
2026-06-13 00:50:49 +02:00
shiki8826 bd8883e5d1 日本語翻訳追加 2026-06-09 23:11:30 +09:00
shiki8826 b094d6ccd3 日本語翻訳追加 2026-06-09 22:57:47 +09:00
Taveon Nelson 6c257b2080 HCE Savepoint
This is HCE before companion merge
2026-05-26 03:36:45 -05:00
Taveon Nelson 04ea584c4f Delete problems-report.html
removing useless files
2026-05-26 03:20:04 -05:00
nacabaroandGitHub af27fc4933 Merge pull request #49 from Cantalapiedra/patch-1
Update README.md
2026-05-24 21:19:05 +02:00
nacabaroandGitHub 1fffab25bd Merge pull request #51 from lightheel/main
Merging battle system into repo.
2026-05-24 21:18:00 +02:00
Taveon Nelson 1a83527590 speeding up HCE 2026-05-18 17:28:24 -05:00
Taveon Nelson 431be5faea Battlescreen login loop fix 2026-05-18 05:46:40 -05:00
Taveon Nelson 9aa62f0d81 companion reup 2026-05-18 05:17:08 -05:00
Taveon Nelson 8fd4f6c414 VBH is the companion app now
Got tired of having to reinstall all three apps so i ported the companion apps features to vitalwear, now you only need VBH and Vitalwear.

I've also added emblems to items in icons so user isnt confused.
2026-05-18 04:57:57 -05:00
457R0 f829efae68 Overwrite with local version 2026-05-03 18:07:07 -05:00
Taveon Nelson 895b704c8d Make VBH-VW transfer DB owner via provider and shared transfer DAO 2026-04-21 18:57:38 -05:00
Taveon Nelson d40428ccfd Reduce duplicate HCE scans and suppress NFC redispatch 2026-04-21 15:00:14 -05:00
Taveon Nelson cd9a6d5d80 Add VitalWear HCE reader client 2026-04-21 14:41:06 -05:00
Taveon Nelson e0a086d9aa This patch enables transfer between vitalwear and VBH 2026-04-21 14:36:27 -05:00
Taveon Nelson e787ea4a19 Initial commit of VBHelper 2026-04-18 17:19:34 -05:00
Taveon Nelson 5273bae7b6 Update: changes and new files from local development 2026-04-18 07:24:06 -05:00
Carlos P CantalapiedraandGitHub 706f30ea49 Update README.md
Step 2 "VB Arena" should read "VB Helper"
2026-04-03 10:15:36 +02:00
lightheel 3f29d725ea Updated opponent list to show display name. 2026-03-06 15:16:43 -05:00
lightheel 977896d296 Merge bugfixes. 2026-03-06 12:20:28 -05:00
lightheel 654529c851 Merge remote-tracking branch 'upstream/main' 2026-03-06 10:56:32 -05:00
nacabaro 366f425539 Updated libraries
Moved library definitions to the catalog, and updated the catalog
2026-01-28 02:05:43 +01:00
nacabaro 37076a9bf0 VERSION UP 2026-01-25 20:00:55 +01:00
nacabaroandGitHub 8c99046edb Merge pull request #47 from nacabaro/specialMissions/fix
Fix Special Missions a bit more
2026-01-25 19:58:39 +01:00
nacabaro c990a495b7 Fix Special Missions a bit more
Now they should behave a bit better, and it will not allow the watch to have multiple missions of the same type, plus you can now delete special missions.
2026-01-25 19:58:03 +01:00
nacabaroandGitHub 0e61723db1 Merge pull request #46 from nacabaro/ui/cardIcon
Card icon in home screen
2026-01-25 17:53:30 +01:00
nacabaro 8815907563 Card icon in home screen
Together with character sprite
2026-01-25 17:35:54 +01:00
lightheel 566e2ec977 Battles now update wins/losses in room DB. 2026-01-21 22:53:42 -05:00
lightheel 65a7ccb221 Fixed resume battle max HP display bug. 2026-01-21 21:27:11 -05:00
lightheel 2201b7d0fe Setup resume/quit options if match gets disconnected before it finishes. 2026-01-21 20:23:51 -05:00
lightheel a9c354ad8a Updated to use Long instead of Int on playerID. 2026-01-19 22:21:05 -05:00
lightheel 9365bc0215 API calls now use X-Session-Token. 2026-01-19 22:16:09 -05:00
lightheel efa4bab144 Merge remote-tracking branch 'upstream/main' 2026-01-18 20:37:58 -05:00
nacabaroandGitHub 6cce33c5f6 Merge pull request #44 from jeffersoncarlospedroso/feature/add-multilanguage-support
Add multilanguage support (i18n) initial implementation
2025-12-14 12:31:27 +01:00
jeffersoncarlospedroso 16fbfae0c2 feat(i18n): continue translation work 2025-12-11 22:38:43 +00:00
jeffersoncarlospedroso 0d174f1550 Add multilanguage support (i18n) initial implementation 2025-12-10 23:06:37 +00:00
lightheel eb82c2afc1 Added BGM to battles. 2025-11-17 17:52:53 -05:00
lightheel c774fd1536 Adjusted auth to not force login every time app is reopened. 2025-11-17 17:34:58 -05:00
lightheel 54b2905196 Changed to use small attack sprites instead of large. 2025-11-17 17:25:57 -05:00
lightheel c4450296db Removed debugging logging. 2025-11-17 17:22:25 -05:00
lightheel d1908d629a Fixed logout when swapping from screen orientation. 2025-11-17 16:52:07 -05:00
lightheel 67b56b3990 Reduced logging. 2025-11-17 16:37:59 -05:00
lightheel b0c5d6375d Setup API calls to use User ID. 2025-11-17 15:06:42 -05:00
lightheel 29ff2805c3 Added NacaAuth for logging into Battles. 2025-11-17 14:31:18 -05:00
lightheel b4d509aad9 Swapped from using internal app storage to android device storage. 2025-11-17 11:11:58 -05:00
lightheel 44c6382356 Merge remote-tracking branch 'upstream/main' 2025-11-17 10:26:59 -05:00
nacabaro 489e27b038 More things
- Active characters display different
- Adventure missions award credits (same as special missions)
- Clicking on about will open the github page
- Corrected from arean to arena
- Removed rename from the settings (it was not used)
2025-11-16 21:59:08 +00:00
nacabaro 6be167bbed Might be incomplete uhhh 2025-11-16 21:13:28 +01:00
nacabaroandGitHub 61dfc2ce0d Merge pull request #43 from nacabaro/screen/upload_screen
Other few things
2025-11-16 01:35:31 +01:00
nacabaro dce186737d Other few things
- I changed more things to flows from the database
- Cleaned up the logic coming from the scan screen
- Added a delete button to a character. CAREFUL, IT HAS NO CONFIRMATION YET!
- Fixed a few things, now scanning is more stable and will fix the second whoops thing.
- Quick patch, should improve stability when writing to the watch
2025-11-16 01:34:31 +01:00
nacabaroandGitHub cf272e8030 Merge pull request #42 from nacabaro/dex/possible_fusions
Few things
2025-11-15 20:12:36 +01:00
nacabaro ffa6958a89 Few things
- Finished working with the card fusions
- Added the GUI part to access the data
- Cleaned up a bit of the code, separated a few things from the SettingsScreenControllerImpl.kt into separate classes.
- Removed a few LaunchedEffect in exchange of Flows (UI now updates automatically when an action happens)
2025-11-15 20:10:17 +01:00
nacabaroandGitHub 1d9879fb50 Merge pull request #41 from nacabaro/store/updated_store
Updated the store
2025-11-15 10:23:07 +01:00
nacabaro 3fa072ce1e Updated the store
- Items can now be purchased
- Changed settings to make use of flows/stateflows from the database, which update the screen in real time after any changes.
- Separated functions from files
2025-11-13 23:52:14 +01:00
nacabaro 69245be8dd Updated README.md 2025-11-03 13:20:16 +01:00
lightheel 0f1feb88b8 Update .gitignore 2025-10-19 17:36:20 -04:00
lightheel 5ddb8f5da9 Battle screen now filters opponents automatically based on active Digimon's stage. 2025-10-19 13:32:41 -04:00
lightheel 0875b114d5 Fixed HP desync with battle. 2025-10-19 13:09:14 -04:00
lightheel 61daad459b Setup player API call to pull from active Digi. 2025-10-19 12:55:57 -04:00
lightheel 14e941031c Updated API to send charaID instead of character name for match setup. 2025-10-19 12:14:32 -04:00
lightheel 952fd5a871 Added scrollable button list for opponents of all stages. 2025-10-19 12:02:14 -04:00
lightheel ac02205f76 Updated sprites to load from phone's internal storage. 2025-10-19 10:20:01 -04:00
lightheel 68ad57b78f Update .gitignore 2025-10-19 08:24:50 -04:00
Nacho 73c393df64 Fixed bug syncing watch adventure missions progress with the app 2025-09-14 00:34:05 +02:00
nacabaroandGitHub 0b3ce486cb Merge pull request #39 from nacabaro/card/import_more
Adventure Missions progress
2025-09-10 02:10:22 +02:00
Nacho 7160eb792a Adventure Missions progress
- Viewing the adventure missions completed in a card is now possible.
2025-09-10 02:09:15 +02:00
lightheelandGitHub 07983d9403 Merge branch 'nacabaro:main' into main 2025-09-05 11:42:43 -04:00
Nacho cb9fe8e716 Merge remote-tracking branch 'origin/main' 2025-09-05 02:22:09 +02:00
Nacho 540766eefb VERSION UP 2025-09-05 02:21:47 +02:00
nacabaroandGitHub af2c852bac Merge pull request #38 from nacabaro/vb/special_missions
Small fix related to special missions
2025-09-05 02:19:36 +02:00
Nacho 8dc5bbbdde Small fix related to special missions
- Now you can clear failed special missions. When a special mission fails, you will not get an item after clearing it.

- Huge note: I have not tested this fix, but it should work... At least that's what my intuition is telling me!

Jokes aside, this should work, but the home screen will not update. I should update this to make use of StateFlows and Flows to keep the home screen updated at all times.
2025-09-05 02:19:09 +02:00
nacabaroandGitHub aac05a3b1c Merge pull request #37 from nacabaro/card/import_more
Agarrate, que vienen curvas
2025-09-05 02:10:05 +02:00
Nacho b827fdccbe Agarrate, que vienen curvas
- Importing more data from the card, this time it's to enhance the functionality of the dex. Things that I'm importing:
  - Tropies
  - Vitals
  - WinRate
  - Battles needed
  - Adventure level cleared
- This commit also includes displaying the data, modifying the relational model and modifying the underlying import functions.

- I've also changed the name from Character to CharacterData, since there is another class that is also called Character. I wanted to move it into the card package, since it's more related to Cards, while character is more designed for user raised characters.

- Another small addition was the ability to construct ImageBitmaps from the BitmapData.kt class. reduces code and makes it more simple, will refactor the old code to use this later on.

- Things I might do
  - Move out of the SettingsScreenControllerImpl.kt file all the card import stuff, since it's slowly growing a lot, and it could benefit from some independence
2025-09-05 02:08:44 +02:00
Nacho 2586190e5c Update libraries 2025-09-02 18:01:11 +02:00
Nacho 1a6753d2aa Fixed date issues with TransformationHistory 2025-09-02 04:54:08 +02:00
lightheelandGitHub 9c581a5ebd Merge pull request #1 from lightheel/vb_battle_client
Vb battle client
2025-08-15 15:46:35 -04:00
lightheelandGitHub 0add667ef8 Merge branch 'nacabaro:main' into vb_battle_client 2025-08-15 15:44:42 -04:00
lightheel c122b71b46 Changed references to be correct. 2025-08-15 11:55:45 -04:00
Nacho f8ec899335 Version up and credits 2025-08-09 12:59:01 +02:00
Nacho b95edfa2a1 New icon 2025-08-09 12:55:38 +02:00
lightheel 5ed7d117f5 Fixed background scaling issues in landscape view. 2025-08-08 13:20:40 -04:00
lightheel 16cd7abce8 Added multiple background sets for battle. 2025-08-08 12:37:32 -04:00
lightheel 6a6369ae9e Adjusted attack hit sprite location in landscape view. 2025-08-08 12:17:47 -04:00
lightheel 1bbaf66c24 Removed exit button from view until battle is over. 2025-08-08 09:24:30 -04:00
lightheel ba03be808e Removed attack bar and button from player view. 2025-08-08 07:51:51 -04:00
lightheel bb1c29bbb4 Moved sprites to load from external Android storage.
Setup landscape view for battle.
2025-08-08 07:44:36 -04:00
lightheel f0760f9ed0 Added gray boxes around HP 2025-08-07 11:23:32 -04:00
lightheel 61a2439f84 Adjusted horizontal sprite shake timing to sync with attack hit. 2025-08-06 19:19:58 -04:00
lightheel 5bb9fe5209 Delayed damage text to show up when attack sprite hits. 2025-08-06 18:59:08 -04:00
lightheel 26fc0ced56 Adjusted hit effect timing. 2025-08-06 18:53:23 -04:00
lightheel 371a850d45 Offset idle animation so player/enemy Digimon aren't in sync. 2025-08-06 18:38:11 -04:00
lightheel d833a89c17 Added hit sprites. 2025-08-06 18:03:03 -04:00
Nacho 1a570170ca Merge remote-tracking branch 'origin/main' 2025-08-06 20:18:43 +02:00
Nacho 1566876106 Version up 2025-08-06 20:18:32 +02:00
nacabaroandGitHub 019f07d827 Merge pull request #36 from nacabaro/card/card_management
Few things here and there
2025-08-06 20:13:09 +02:00
Nacho 4474e6689d Few things here and there
- Card management, you can now modify the name of the cards and remove cards too.
- Also, support for multiple cards with the same ID works too. When scanning for the first time, if a repeat card exists, the user will be asked to choose which card the character comes from. Future scans will not ask since it is stored in the watch app reserved area.
2025-08-06 20:10:21 +02:00
lightheel 28cb824bf3 Changed battle background to use all 3 layers. 2025-08-06 12:36:21 -04:00
lightheel 0a643053af Adjusted HP text color and size. 2025-08-06 12:21:30 -04:00
lightheel 5989f48355 Added battle background. 2025-08-06 11:51:37 -04:00
lightheel de3d312a32 Fixed pop up damage timing and positioning. 2025-08-06 10:49:30 -04:00
lightheel cd33d06ecf Added pop up damage number. 2025-08-06 10:25:26 -04:00
lightheel 6ea9946412 Attack sprites now shows on main battle screen. 2025-08-06 09:00:51 -04:00
lightheel ec3058b511 Adjusted dodge trigger. 2025-08-05 20:03:50 -04:00
lightheel 26842d1b1b Reduced attack button size. 2025-08-05 19:55:36 -04:00
lightheel b74b04cda9 Fixed sprite positioning on main screen. 2025-08-05 19:50:37 -04:00
lightheel 481c0b6d9a Adjusted BattlesScreen UI. 2025-08-05 19:42:41 -04:00
lightheel a3bebcf290 Adjusted animation timings. 2025-08-05 19:42:31 -04:00
Nacho fb09004d13 Version up, again 2025-08-06 01:32:10 +02:00
Nacho dd1a62d184 Made a mistake
Accidentally locked BE watch items to only BEm characters. I have modified it to also include DiM characters in the BE.
2025-08-06 01:31:18 +02:00
Nacho f96f00e05a Version up
And change the original pop up to mention this works with VB, my bad
2025-08-06 01:22:43 +02:00
nacabaroandGitHub 3c17e93d0f Merge pull request #35 from nacabaro/vb/special_missions
Special missions
2025-08-06 01:21:00 +02:00
Nacho f8ce81e932 Special missions
- Basic implementation is complete
- Added 8 different special missions, 4 are easy and more expensive, and 4 are cheaper and more difficult
- Added 9 missions of each so people can test themselves
- Also added checks to disallow BE digimon to have special missions
- UI elements to display the status of the missions
- Finishing a mission awards a random price (TODO: Make the price be based on the mission difficulty)
2025-08-06 01:19:19 +02:00
lightheel b3cf823c3f Swapped to 3 panel battle. 2025-08-05 18:35:50 -04:00
lightheel 7843be7004 Fixed dodge animation. Sprites now move up then back to original location. 2025-08-05 17:35:44 -04:00
lightheel f0f1d9830e Added dodge and attack hit animations. 2025-08-05 16:14:17 -04:00
lightheel c404f4f436 Raised opponent HP bar. 2025-08-05 07:01:39 -04:00
lightheel 71ba5e0207 Fixed atk sprite import bug. Readjusted sprites to load from individual files instead of full spritesheets. 2025-08-05 06:54:55 -04:00
lightheel fb09350825 Created sprite and animation tester menu. 2025-08-04 14:26:24 -04:00
lightheel 615fb85204 Added sprite test button to battle. 2025-08-04 14:13:26 -04:00
Nacho 7bb7693876 Version Up 2025-08-04 19:57:39 +02:00
nacabaroandGitHub 2b33042fe0 Merge pull request #34 from nacabaro/vb/nfc_compat
Added basic VB support
2025-08-04 19:56:12 +02:00
lightheel 3687ff2c21 Updated idle animation to use 2 sprites instead of just 1. 2025-08-04 13:26:39 -04:00
lightheel c973030d9d Fixed animation mapping.
Was previously using _00, _01, _02, etc. number suffix at end of .json file.

Actual sprite index is stored as the m_Name field inside this file.
2025-08-04 13:23:04 -04:00
lightheel 9f7e452850 Added Digimon idle animation. 2025-08-04 13:00:16 -04:00
lightheel 37e5efa874 Started setting up sprite animations. 2025-08-04 12:57:35 -04:00
lightheel c5cebd8213 Fixed bug where enemy attack was showing as player attack sprite for a moment before animation. 2025-08-04 12:21:41 -04:00
lightheel cfa52bce9b Fixed enemy attack sprite positioning. 2025-08-04 12:14:29 -04:00
lightheel acd990d32b Adjusted player attack sprite positioning. 2025-08-04 12:06:46 -04:00
lightheel c8690152bc Fixed player attack sprite on enemy screen.
Was showing enemy attack sprite.
2025-08-04 12:02:41 -04:00
lightheel eff2fadb55 Enemy attack sprite now shows on player screen. 2025-08-04 11:32:41 -04:00
lightheel 8f790eea41 Fixed player Digimon positioning. 2025-08-04 11:25:01 -04:00
lightheel 9d0e68fb8a Re-added missing UI pieces (HP bar, HP numbers, & exit button). 2025-08-04 11:22:22 -04:00
lightheel 31fab9dba4 Fixed damage application timing. 2025-08-04 11:13:01 -04:00
lightheel 7af8e00e6f Re-added missing exit button and health bars. 2025-08-04 11:02:51 -04:00
lightheel 4e12962c05 Fixed sprite naming bug. 2025-08-04 10:48:21 -04:00
lightheel b3a4ced28d Commented out crit bar progress to stop log noise. 2025-08-04 10:48:06 -04:00
lightheel d7b10b1ae8 Update BattlesScreen.kt 2025-08-04 10:04:30 -04:00
lightheel 942d843601 Create ArenaBattleSystem.kt 2025-08-04 10:04:05 -04:00
Nacho 4d8dcde26e Forgot to add all the UI methods to preview VB characters 2025-08-04 13:15:07 +02:00
lightheel 3b762d6195 Updated battle loop. Started to fix attack animations. 2025-08-03 15:25:37 -04:00
lightheel 023af17b23 Updated BattleScreen to perform full battle loop with API calls. 2025-08-03 13:23:46 -04:00
lightheel 2901bcf0da Updated attack button to actually perform stage 1 API call. 2025-08-03 12:39:03 -04:00
lightheel 266658342a Fixed sprite sizing. 2025-08-03 12:14:24 -04:00
lightheel a4b159da45 Sprites now load correctly for Digimon and attacks. 2025-08-03 11:37:43 -04:00
lightheel fa8546f283 Updated file and sprite managers to use new file structure. 2025-08-03 11:29:40 -04:00
lightheel d86ee00109 Update .gitignore 2025-08-03 11:29:23 -04:00
lightheel 6c9d057917 Updated client for attack battle loop. 2025-08-02 07:49:43 -04:00
lightheel a044d24f5f Updated BattleScreen.kt. Now starts battle and allow clicking of first attack button. 2025-08-02 06:26:27 -04:00
lightheel 1d21155198 Create AttackSpriteManager.kt 2025-08-02 06:26:01 -04:00
lightheel 09ee139add Create BattleSpriteManager.kt 2025-08-02 06:25:59 -04:00
lightheel 8d9c507645 Create SpriteFileManager.kt 2025-08-02 06:25:57 -04:00
lightheel c947e2519c Create AttackSpriteImage.kt 2025-08-02 06:25:54 -04:00
lightheel 9bcbe85b7f Create SpriteImage.kt 2025-08-02 06:25:48 -04:00
lightheel a72718273c Added imports to remove build errors. Started added battle screens, logic, and animations. 2025-08-01 17:04:35 -04:00
lightheel bd0cc46398 Setup API calls for stage 0 on Digimon name button click. 2025-08-01 15:53:40 -04:00
lightheel a011ae39a4 First push on branch. 2025-08-01 15:42:37 -04:00
Nacho 0b1eed7da9 Increase version 2025-07-29 23:08:45 +02:00
Nacho 36c3e25fd3 Last minute bug related to sending the active character from the main screen. 2025-07-29 19:22:23 +02:00
nacabaroandGitHub ac05dfc541 Merge pull request #33 from nacabaro/vb/nfc_compat
Add VB Compatibility
2025-07-29 18:42:10 +02:00
Nacho 220a61a553 Bug fixes
- Fixed an issue with the calendar, now I store the date as year/month/day in the database directly.
- VB expects to have a year between 2021 and 2032, for some reason it gives me back a 2000 year when reading an empty history. I just patched it in the application to return as year 0 in those cases.
- VitalsHistory works as intended now. Also verified functionality of the new classes.
- Moved a few SQL queries too, since they didn't belong where they were.

Also added a small feature, now the application knows if you entered the scan screen through a character in the storage, or the home screen. If there is an active character and you opened the home screen, the application will let you send the active character, while if it was opened through the storage screen, the application will send the character from the storage screen.

What's now missing is to create a VBSCanScreen, and depending on which device we are using, make use of one screen or the other.
2025-07-29 10:36:07 +02:00
Nacho d847f600f1 Quite a few things
- Renamed a few objects to avoid legality issues
- Added a few accompanying tables to support the VB/VH
  - While we speak about tables, added an auxiliary table to track card adventure progress
- Extracted NFC character generation and extraction to different classes, should result in a cleaner ScanScreenController
2025-07-29 02:50:15 +02:00
Nacho f7a4039d66 Merge branch 'main' into vb/nfc_compat
# Conflicts:
#	app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt
2025-07-29 01:35:21 +02:00
nacabaroandGitHub 1eed6a4c17 Merge pull request #32 from nacabaro/navigation/animations
Graphical things
2025-07-29 01:29:55 +02:00
nacabaroandGitHub 1a09e40d1d Merge pull request #31 from nacabaro/database/get_all_sprites
Fetch all the character sprites into the internal database
2025-07-29 01:26:49 +02:00
Nacho 13051145b7 Few things:
- Speed up the fade animation a tad bit
- Added a back arrow in the scan screen
- Improved a bit the placeholder of the battles screen, since it'd throw out the transition animation.
2025-07-29 00:01:02 +02:00
Nacho 1a150ce7ce Few things:
- Speed up the fade animation a tad bit
- Added a back arrow in the scan screen
- Improved a bit the placeholder of the battles screen, since it'd throw out the transition animation.
2025-07-28 23:58:41 +02:00
Nacho 39ba48d867 Forgot to remove the dependency to SpriteRepo 2025-07-27 19:46:07 +02:00
Nacho 3f324ca141 SpriteRepo is not needed anymore 2025-07-27 19:44:12 +02:00
Nacho 1e28a8c249 Application now imports all the sprites related to the character into the internal database. 2025-07-27 19:42:25 +02:00
Nacho d46769b0cb Added credits for the app in preparation for version 0.1 2025-07-26 22:26:41 +02:00
Nacho fce05870c5 This should allow the app to build, otherwise it won't or it will crash 2025-07-26 14:23:53 +02:00
Nacho 9f5f806786 Added basic stuff for compatibility with VB 2025-06-01 18:51:30 +02:00
nacabaroandGitHub d749af0a9c Merge pull request #27 from cfogrady/FixStageAndAttributeFromCharacter
Fix stage and attribute coming from character instead of UserCharacter
2025-04-08 15:35:13 +02:00
Christopher O'Grady 076f6bfacf Fix stage and attribute coming from character instead of UserCharacter 2025-04-05 20:31:03 -04:00
nacabaroandGitHub a4f3b701e5 Merge pull request #26 from cfogrady/RemoveDuplicatedData
Remove duplicated stage and attribute from UserCharacter
2025-03-22 00:34:07 +01:00
Christopher O'Grady da134247b1 Remove duplicated stage and attribute from UserCharacter
These fields are on the Character entity.
2025-03-19 15:33:52 -04:00
Nacho f7b3b7256a VB NFC compatibility
- Refactored some names (not really relevant)
- Added the ability to store special missions inside the application's database
- Refactored the conversion code into two classes inside the scan screen package
- Added the missing tables to store the necessary vb data

Also not relevant to this update
- Updated adventure progress app wide, so that instead of it being stored in a character basis, it is shared across all characters in the same dim
2025-02-10 17:44:13 +01:00
nacabaroandGitHub e36a700d9f Merge pull request #25 from nacabaro/database/adventure
Adventure mode
2025-01-26 02:01:24 +01:00
Nacho fb6b6d2825 While I'm at it...
- Resized some buttons in the dialogs
- Added a dismiss button in get item dialog after adventure and in the send on adventure choose time dialogs
- Moved the export/import data lower in the settings screen
2025-01-26 01:59:43 +01:00
Nacho e17f6c23e4 Renaming terms and other bits and bobs
- Changed adventure database so that the original intended time is also stored with the final time. This will come useful once the algorithm for determining which object to give is made.
2025-01-26 01:40:32 +01:00
Nacho 409474b5d1 Phew
- Added adventure screen, dialogs, components and controllers needed to rock.
- Modified DTOs so that it is possible to tell if a character is in adventure or not
- Updated layout of items database
- Fixed an issue with importing bems where the character name would be read incorrectly
- Also added a ton of checks
2025-01-26 01:25:25 +01:00
nacabaroandGitHub 547318864b Merge pull request #24 from nacabaro/cleanup
Bigger icons plus material obscurity
2025-01-21 13:35:43 +01:00
Nacho 5e92895656 Made item icons a bit smaller in order to be similarly sized to character icons 2025-01-21 13:34:49 +01:00
Nacho 25500e9cb4 Made character icons bigger and also made the obscured characters follow material guidelines, as being pure black can be difficult to see in dark theme 2025-01-21 13:28:13 +01:00
nacabaroandGitHub 1a38cefb92 Merge pull request #23 from nacabaro/cleanup
Cleanup MainActivity
2025-01-21 13:12:38 +01:00
Nacho a974bd366e Moved read character from watch to its corresponding controller 2025-01-21 13:02:45 +01:00
Nacho bb5f66d167 Moved import card logic outside of the MainActivity 2025-01-21 12:42:52 +01:00
nacabaroandGitHub 09394871e5 Merge pull request #22 from nacabaro/database/items
Add items functionality
2025-01-20 15:49:22 +01:00
Nacho 972a640394 Added missing icons, ready to merge! 2025-01-20 15:46:53 +01:00
Nacho 35a7c9d959 Slowdowns and UI
- Current item can be seen from a character when clicked on it, you can also see how much time left does that item have on that character.
- Fixed slowdown issue in the items screen. Also fixed this issue in the storage screen as it'd also affect it if there are a lot of characters in the storage.
2025-01-20 15:23:40 +01:00
Nacho b4a73031e7 Working items
- Click on use item > then choose the mon you want to apply the item to
2025-01-20 14:45:28 +01:00
Nacho 3e41f82273 Forgot to push the updated db 2025-01-20 00:29:07 +01:00
Nacho f3740d4816 Here comes the train (with more things)
- Updated available items
- Created ItemsScreenController
- Moved the ItemsScreen to keep code organized
2025-01-20 00:06:42 +01:00
Nacho 305b776b52 Things
- Added items store and a way to switch between the store and your items
- Added an items dialog, click on it to see details of the item (description, amount) and use it
- Added items store, it lists all the items available
2025-01-19 13:41:50 +01:00
Nacho c0a67e382b Things items related
- Added a MyItems screen
- Added an ItemElement placeholder
- Included database with all the items in the app (not final)
- Also included some demo items

As of now items don't do anything, i'm still making the UI
2025-01-18 23:44:38 +01:00
nacabaroandGitHub bc7a05ccd3 Merge pull request #21 from nacabaro/db/data_export
Forgot to include the settingsscreencontroller interface my bad
2025-01-18 16:02:29 +01:00
Nacho 660dbaca19 Forgot to include the settingsscreencontroller interface my bad 2025-01-18 16:01:42 +01:00
nacabaroandGitHub b89dfb0df3 Merge pull request #20 from nacabaro/db/data_export
Added data importing/exporting
2025-01-18 12:11:58 +01:00
Nacho 23e233227a Finished refactoring the old SettingsScreenController.kt 2025-01-18 12:05:10 +01:00
Nacho 542072c238 Forgot to close DB before importing process
This will clear the database lock file before closing the application, ensuring that during the second startup the application does not see anything weird with the data. Not closing the database before import can lead to weird behavior.
2025-01-18 01:05:45 +01:00
Nacho 42dd87f0c4 Data export working
During export the database will be closed, otherwise the WAL will ruin the export. data will be saved as a .vbhelper file, then the app will be closed to avoid making any changes to the DB
For import, we check if the extension is .vbhelper, otherwise we can corrupt the database, IMPORTANT. Next we delete everything related to RoomDB and we swap it with the new files. Finally the app will be closed to ensure RoomDB is running on the new DB.

Finally, I still have to reintegrate the importApk functionality with the NewSettingsScreenController and get rid of the old one.
2025-01-18 00:45:16 +01:00
nacabaroandGitHub 635be60cdb Merge pull request #19 from nacabaro/dex/functionality
Dex stuff
2025-01-17 21:21:35 +01:00
Nacho 88163684ca Dex stuff
- Added dex domain model, and rearranged class
- Added dex functionality to the dex screen, and updated the corresponding controller
2025-01-17 21:12:41 +01:00
nacabaroandGitHub 586d6c01ef Merge pull request #18 from nacabaro/ui/home_screen
A lot more things
2025-01-16 01:05:29 +01:00
nacabaroandGitHub 5d996edc1e Merge branch 'main' into ui/home_screen 2025-01-16 01:05:21 +01:00
nacabaroandGitHub 074ced1082 Merge pull request #17 from cfogrady/ObscuredDex
Algorithm to obscure undiscovered digimon
2025-01-16 01:04:32 +01:00
Nacho 5a1d52aa1d A lot more things
- Moved scan button to inside the home screen
- Added items screen
- Scanning functionality updated to work with BEms
- Different home screen layouts depending on the configuration (BE with BEm, BE with DIm, since they don't have the same data, looking at you, special training)
- It is now possible to import BEms
- Character size is now more accurate, (no more big babies)
- Once sent to the watch, characters are deleted from the app, and it shouldn't fail
- Updated domain model to support evolution history.

TODO:
- Reorganize some of the code, mostly SQL queries and reorganize the scan screen functionality
- Create home layout for the VB watch
- Start figuring out reading data from the VB
    - Also create VB data domain model
    - Also start figuring out writing to the VB (it's 3 steps)
- Block off scan button until secrets are imported
- Start working with the dex
    - Update domain model
    - Use cfogrady's blank character when not seen
- Cancel character upload in case the app database does not contain the card for it (otherwise, the watch will delete the character)
- Export character data (in case of any app issues or changing phones)

(Also added some more icons)

Woo
2025-01-16 01:03:57 +01:00
Christopher O'Grady 98bd74c544 Algorithm to obscure undiscovered digimon
Logic still needs to be implemented to distinguish discovered from undiscovered.
2025-01-12 21:36:02 -05:00
Nacho ce1cf3eddb Quicm fix, DisposableEffect isn't reloading when isDoneSendCard is true
TODO: add more disposableeffects
2025-01-12 13:50:41 +01:00
nacabaroandGitHub 0bd745ac86 Merge pull request #16 from nacabaro/ui/home_screen
UI/home screen
2025-01-12 10:23:49 +01:00
nacabaroandGitHub 0d999925b5 Merge pull request #14 from cfogrady/home_screen
Fix arrays for NFCCharacter
2025-01-12 10:23:09 +01:00
nacabaroandGitHub dd893a08da Merge pull request #15 from nacabaro/ui/home_screen
A few things, again...
2025-01-12 10:22:57 +01:00
Christopher O'Grady c5e8236e19 Fix arrays for NFCCharacter 2025-01-11 22:57:07 -05:00
Nacho cac5198488 A few things, again...
- Added ability to write character (incomplete)
- Renamed BottomNavItem.kt to NavigationItems.kt, as it covers the entire application navigation, not just the bottom navigation bar
- Modified the ScanScreenController and the Impl to accomodate writing characters on the BE (need to do the VB later on)
- Modified repositories to fetch data from the database and additional information needed to convert back to NfcCharacter
- Function to convert to NfcCharacter

Originally this was also going to cover the home screen, since my idea was to have marked as active (the one shown in the home screen) be the one sent to the watch, but for testing I have added a "send to bracelet" button on the pop-up on the storage screen.
2025-01-12 00:57:34 +01:00
nacabaroandGitHub 7f22650601 Merge pull request #13 from cfogrady/TransparentBackground
Make background on characters and icons transparent.
2025-01-10 23:10:04 +01:00
Christopher O'Grady 1088e5259a Make background on characters and icons transparent. 2025-01-10 14:24:12 -05:00
nacabaroandGitHub eabf8770cc Merge pull request #12 from cfogrady/IntegrateSecretsWithApp
Integrate secrets with app
2025-01-10 19:38:26 +01:00
nacabaroandGitHub da5e6c6628 Merge pull request #11 from cfogrady/SecretsRepo
Add Secrets Repo Using a Proto DataStore
2025-01-10 19:38:13 +01:00
nacabaroandGitHub 4c0e2fafd0 Merge pull request #10 from cfogrady/APK_Import
APK Import
2025-01-10 19:38:01 +01:00
Christopher O'Grady 4529925906 Integrate SecretsRepository and ApkImporter with app
Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen
2025-01-10 13:21:53 -05:00
Christopher O'Grady c456d455ef Implement Secrets Repository
Add proto plugin and dependencies
Create Secrets Proto
Create Secrets Proto DataStore
Replace old secrets with proto secrets
Fix importers and tests to use new proto secrets.
2025-01-09 15:24:10 -05:00
Christopher O'Grady 9871f0420f Verify Secret correctness as part of loading.
Improve tests
2025-01-08 22:12:03 -05:00
Christopher O'Grady f4974c8705 Import APK in tests 2025-01-08 21:01:37 -05:00
nacabaroandGitHub 5d6e3743f0 Merge pull request #9 from nacabaro/nfc/upload_mon_new_model
Removed unused imports that'd cause compile issues.
2025-01-08 02:37:01 +01:00
Nacho b91da136df Removed unused imports that'd cause compile issues. 2025-01-08 02:34:46 +01:00
nacabaroandGitHub e4366ce15c Merge pull request #8 from nacabaro/nfc/upload_mon_new_model
Nfc/upload mon new model
2025-01-08 02:33:22 +01:00
Nacho 7582f944b5 Forgot to actually remove the files, my bad 2025-01-08 02:32:13 +01:00
Nacho a028611d8a More things:
- Improved upload function to support VB (still to do to add VB)
- Adapted to the latest version of the vb-nfc-reader library
- Added foreign keys with uploaded characters and existing dim database
- Removed the tempoorary objects
- Handled errors uploading (dim not existing for now and VB not supported yet)
- Improved bitmap conversion by moving it to it's own composable
  - Also added object to move bitmap data around
- Added DTO to read sprite data and character data simultaneously

Good night!
2025-01-08 02:31:36 +01:00
nacabaroandGitHub 5edd753da1 Merge pull request #7 from nacabaro/dim/read_cards
Read cards
2025-01-05 22:24:31 +01:00
278 changed files with 22359 additions and 1490 deletions
+11
View File
@@ -6,3 +6,14 @@
local.properties local.properties
app/src/main/res/values/keys.xml 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
+66 -35
View File
@@ -1,38 +1,69 @@
# VBHelper # 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) ## Current state of the project
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) Right now the project is still under development, and until further notice, any database updates will result in having to erase application data.
4. Run publishToMavenLocal gradle task in the vb-dim-reader project.
5. Create res/values/keys.xml within the app module. This document will be updated once the application does not need any more database resets.
6. Populate with:
``` ## Features
<?xml version="1.0" encoding="utf-8"?>
<resources> As of now, the project allows you to read characters, view characters stats, and send them back to your watch.
<string name="password1">beHmacKey1</string>
<string name="password2">beHmacKey2</string> You can also apply items to the characters read, such as special missions, or change timers, and store characters in the storage section.
<string name="decryptionKey">aesKey</string>
<integer-array name="substitutionArray"> 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.
<item>0</item>
<item>1</item> 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.
<item>2</item>
<item>3</item> ## How to set up
<item>4</item>
<item>5</item> 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:
<item>6</item>
<item>7</item> 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.
<item>8</item>
<item>9</item> 2. Once the files are extracted, look for an APK called `com.bandai.vitalbraceletarena.apk`. Copy it somewhere else, you will need it.
<item>10</item>
<item>11</item> 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.
<item>12</item>
<item>13</item> 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.
<item>14</item>
<item>15</item> 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`.
</integer-array>
</resources> You will be prompted to choose a file. Choose the APK file that was previously obtained.
```
7. Replace the values in the keys.xml file with those extracted from the original app. 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.
8. Run
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.
+63 -17
View File
@@ -1,20 +1,22 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.0.21-1.0.27" alias(libs.plugins.vksp)
alias(libs.plugins.vprotobuf)
} }
android { android {
namespace = "com.github.nacabaro.vbhelper" namespace = "com.github.nacabaro.vbhelper"
compileSdk = 35 compileSdk = 37
defaultConfig { defaultConfig {
applicationId = "com.github.nacabaro.vbhelper" applicationId = "com.github.nacabaro.vbhelper"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 37
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "Alpha 0.6.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -32,37 +34,81 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions {
jvmTarget = "11"
}
buildFeatures { buildFeatures {
compose = true compose = true
} }
lint {
baseline = file("lint-baseline.xml")
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.0"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
} }
dependencies { dependencies {
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.vb.nfc.reader) implementation(libs.vb.nfc.reader)
implementation(libs.dim.reader) implementation(libs.dim.reader)
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx) 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.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.datastore)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) 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) testImplementation(libs.junit)
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) implementation(libs.retrofit)
implementation("androidx.navigation:navigation-compose:2.7.0") implementation(libs.retrofit.converter.gson)
implementation("com.google.android.material:material:1.2.0") implementation(libs.gson)
implementation("androidx.compose.material:material")
} // HTTP request logging
implementation(libs.okhttp.logging.interceptor)
}
File diff suppressed because it is too large Load Diff
+37 -2
View File
@@ -4,6 +4,12 @@
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" /> <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 <application
android:name=".di.VBHelper" android:name=".di.VBHelper"
@@ -15,18 +21,47 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.VBHelper" android:theme="@style/Theme.VBHelper"
android:usesCleartextTraffic="true"
tools:targetApi="31"> 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 <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.VBHelper"> android:theme="@style/Theme.VBHelper">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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> </activity>
</application> </application>
</manifest> </manifest>
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)
}
}
}
@@ -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"
}
}
@@ -0,0 +1,16 @@
package com.github.nacabaro.vbhelper
interface ActivityLifecycleListener {
fun onPause()
fun onResume()
companion object {
fun noOpInstance(): ActivityLifecycleListener {
return object: ActivityLifecycleListener {
override fun onPause() {}
override fun onResume() {}
}
}
}
}
@@ -1,350 +1,200 @@
package com.github.nacabaro.vbhelper package com.github.nacabaro.vbhelper
import android.content.Intent import android.content.Intent
import android.nfc.NfcAdapter import android.net.Uri
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vb.dim.card.DimReader import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.navigation.AppNavigation import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Dim import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.domain.Sprites import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
import com.github.nacabaro.vbhelper.domain.Character import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory 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 com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer> private var initialRoute: String? = null
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null) private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
if( onActivityLifecycleListeners[key] != null) {
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> throw IllegalStateException("Key is already in use")
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
registerFileActivityResult()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
finish()
return
} }
nfcAdapter = maybeNfcAdapter onActivityLifecycleListeners[key] = activityLifecycleListener
}
private fun unregisterActivityLifecycleListener(key: String) {
onActivityLifecycleListeners.remove(key)
}
override fun onCreate(savedInstanceState: Bundle?) {
val application = applicationContext as VBHelper
val scanScreenController = ScanScreenControllerImpl(
application.container.dataStoreSecretsRepository.secretsFlow,
this,
this::registerActivityLifecycleListener,
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) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
initialRoute = getInitialRouteFromIntent(intent)
setContent { setContent {
VBHelperTheme { VBHelperTheme {
MainApplication() MainApplication(
scanScreenController = scanScreenController,
settingsScreenController = settingsScreenController,
itemsScreenController = itemsScreenController,
adventureScreenController = adventureScreenController,
homeScreenController = homeScreenController,
storageScreenController = storageScreenController,
spriteViewerController = spriteViewerController,
cardScreenController = cardScreenController,
initialRoute = initialRoute
)
}
}
Log.i("MainActivity", "Activity onCreated")
handleImportIntent(intent)
}
override fun onPause() {
super.onPause()
Log.i("MainActivity", "onPause")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onPause()
}
}
override fun onResume() {
super.onResume()
Log.i("MainActivity", "Resume")
for(activityListener in onActivityLifecycleListeners) {
activityListener.value.onResume()
}
}
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 registerFileActivityResult() { private fun extractVitalWearImportUri(intent: Intent?): Uri? {
activityResultLauncher = registerForActivityResult( if (intent == null) {
ActivityResultContracts.StartActivityForResult() return null
) { }
lifecycleScope.launch {
if (it.resultCode != RESULT_OK) {
Toast.makeText(applicationContext, "Import operation cancelled.", Toast.LENGTH_SHORT).show()
}
val contentResolver = applicationContext.contentResolver
val inputStream = contentResolver.openInputStream(it.data!!.data!!)
inputStream.use { fileReader ->
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
val dimModel = Dim(
id = card.header.dimId,
logo = card.spriteData.sprites[0].pixelData,
name = card.spriteData.text, // TODO Make user write card name
stageCount = card.adventureLevels.levels.size,
logoHeight = card.spriteData.sprites[0].height,
logoWidth = card.spriteData.sprites[0].width
)
val application = applicationContext as VBHelper
val storageRepository = application.container.db
storageRepository.dimDao().insertNewDim(dimModel)
val characters = card.characterStats.characterEntries val isVitalWearImport = intent.type == VITALWEAR_CHARACTER_MIME
if (!isVitalWearImport) {
return null
}
/* return when (intent.action) {
Confusing math for me ahead Intent.ACTION_SEND -> {
sprite[0] logo @Suppress("DEPRECATION")
sprite[10] name intent.getParcelableExtra(Intent.EXTRA_STREAM)
sprite[10 + 1] character_1 }
sprite[10 + 2] character_2 Intent.ACTION_VIEW -> intent.data
sprite[16] name 1 else -> null
sprite[17] character_1 }
sprite[18] character_2 }
sprite[23] name 2
sprite[24] character_1
sprite[25] character_2
sprite[23 + 12 + 1] name 3
sprite[23 + 12 + 2] character_1
sprite[23 + 12 + 3] character_2
*/
var spriteCounter = 10 private fun getInitialRouteFromIntent(intent: Intent?): String? {
var domainCharacters = mutableListOf<Character>() if (intent == null) return null
val data = intent.data
for (index in 0 until characters.size) { if (intent.action == Intent.ACTION_VIEW && data != null) {
domainCharacters.add( if (data.scheme == "vbhelper" && data.host == "auth") {
Character( return "Battle"
id = 0,
dimId = card.header.dimId,
monIndex = index,
name = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = characters[index].attribute,
baseHp = characters[index].hp,
baseBp = characters[index].dp,
baseAp = characters[index].ap,
sprite1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
sprite2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
nameWidth = card.spriteData.sprites[spriteCounter].width,
nameHeight = card.spriteData.sprites[spriteCounter].height,
spritesWidth = card.spriteData.sprites[spriteCounter + 1].width,
spritesHeight = card.spriteData.sprites[spriteCounter + 1].height
)
)
if (index == 0) {
spriteCounter += 6
} else if (index == 1) {
spriteCounter += 7
} else {
spriteCounter += 14
}
}
storageRepository
.characterDao()
.insertCharacter(*domainCharacters.toTypedArray())
val sprites = card.spriteData.sprites.map { sprite ->
Sprites(
id = 0,
sprite = sprite.pixelData,
width = sprite.width,
height = sprite.height
)
}
storageRepository
.characterDao()
.insertSprite(*sprites.toTypedArray())
}
inputStream?.close()
Toast.makeText(applicationContext, "Import successful!", Toast.LENGTH_SHORT).show()
} }
} }
return null
} }
@Composable @Composable
private fun MainApplication() { private fun MainApplication(
var isDoneReadingCharacter by remember { mutableStateOf(false) } scanScreenController: ScanScreenControllerImpl,
settingsScreenController: SettingsScreenControllerImpl,
itemsScreenController: ItemsScreenControllerImpl,
adventureScreenController: AdventureScreenControllerImpl,
storageScreenController: StorageScreenControllerImpl,
homeScreenController: HomeScreenControllerImpl,
spriteViewerController: SpriteViewerControllerImpl,
cardScreenController: CardScreenControllerImpl,
initialRoute: String? = null
) {
AppNavigation( AppNavigation(
isDoneReadingCharacter = isDoneReadingCharacter, applicationNavigationHandlers = AppNavigationHandlers(
onClickRead = { settingsScreenController,
handleTag { scanScreenController,
val character = it.receiveCharacter() itemsScreenController,
nfcCharacter.value = character adventureScreenController,
addCharacterScannedIntoDatabase() storageScreenController,
isDoneReadingCharacter = true homeScreenController,
"Done reading character" spriteViewerController,
} cardScreenController
}, ),
onClickScan = { initialRoute = initialRoute
isDoneReadingCharacter = false
},
onClickImportCard = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activityResultLauncher.launch(intent)
}
) )
} }
// EXTRACTED DIRECTLY FROM EXAMPLE APP companion object {
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> { private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
return mapOf(
Pair(DeviceType.VitalBraceletBEDeviceType,
CryptographicTransformer(readableHmacKey1 = resources.getString(R.string.password1),
readableHmacKey2 = resources.getString(R.string.password2),
aesKey = resources.getString(R.string.decryptionKey),
substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
// Pair(DeviceType.VitalSeriesDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
// Pair(DeviceType.VitalCharactersDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
runOnUiThread {
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
val successText = handlerFunc(tagCommunicator)
runOnUiThread {
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onPause() {
super.onPause()
if (nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(this)
}
}
//
/*
TODO:
- Make it able to detect the different model of watches
- Support for regular VB
The good news is that the theory behind inserting to the database should be working
now, it's a matter of implementing the functionality to parse dim/bem cards and use my
domain model.
*/
private fun addCharacterScannedIntoDatabase() {
val beCharacter = nfcCharacter as MutableStateFlow<BENfcCharacter?>
val temporaryCharacterData = TemporaryCharacterData(
dimId = nfcCharacter.value!!.dimId.toInt(),
charIndex = nfcCharacter.value!!.charIndex.toInt(),
stage = nfcCharacter.value!!.stage.toInt(),
attribute = nfcCharacter.value!!.attribute,
ageInDays = nfcCharacter.value!!.ageInDays.toInt(),
nextAdventureMissionStage = nfcCharacter.value!!.nextAdventureMissionStage.toInt(),
mood = nfcCharacter.value!!.mood.toInt(),
vitalPoints = nfcCharacter.value!!.vitalPoints.toInt(),
transformationCountdown = nfcCharacter.value!!.transformationCountdown.toInt(),
injuryStatus = nfcCharacter.value!!.injuryStatus,
trophies = nfcCharacter.value!!.trophies.toInt(),
currentPhaseBattlesWon = nfcCharacter.value!!.currentPhaseBattlesWon.toInt(),
currentPhaseBattlesLost = nfcCharacter.value!!.currentPhaseBattlesLost.toInt(),
totalBattlesWon = nfcCharacter.value!!.totalBattlesWon.toInt(),
totalBattlesLost = nfcCharacter.value!!.totalBattlesLost.toInt(),
activityLevel = nfcCharacter.value!!.activityLevel.toInt(),
heartRateCurrent = nfcCharacter.value!!.heartRateCurrent.toInt()
)
val application = applicationContext as VBHelper
val storageRepository = application.container.db
val characterId: Long = storageRepository
.temporaryMonsterDao()
.insertCharacterData(temporaryCharacterData)
val temporaryBECharacterData = TemporaryBECharacterData(
id = characterId,
trainingHp = beCharacter.value!!.trainingHp.toInt(),
trainingAp = beCharacter.value!!.trainingAp.toInt(),
trainingBp = beCharacter.value!!.trainingBp.toInt(),
remainingTrainingTimeInMinutes = beCharacter.value!!.remainingTrainingTimeInMinutes.toInt(),
itemEffectActivityLevelValue = beCharacter.value!!.itemEffectActivityLevelValue.toInt(),
itemEffectMentalStateValue = beCharacter.value!!.itemEffectMentalStateValue.toInt(),
itemEffectMentalStateMinutesRemaining = beCharacter.value!!.itemEffectMentalStateMinutesRemaining.toInt(),
itemEffectActivityLevelMinutesRemaining = beCharacter.value!!.itemEffectActivityLevelMinutesRemaining.toInt(),
itemEffectVitalPointsChangeValue = beCharacter.value!!.itemEffectVitalPointsChangeValue.toInt(),
itemEffectVitalPointsChangeMinutesRemaining = beCharacter.value!!.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
abilityRarity = beCharacter.value!!.abilityRarity,
abilityType = beCharacter.value!!.abilityType.toInt(),
abilityBranch = beCharacter.value!!.abilityBranch.toInt(),
abilityReset = beCharacter.value!!.abilityReset.toInt(),
rank = beCharacter.value!!.abilityReset.toInt(),
itemType = beCharacter.value!!.itemType.toInt(),
itemMultiplier = beCharacter.value!!.itemMultiplier.toInt(),
itemRemainingTime = beCharacter.value!!.itemRemainingTime.toInt(),
otp0 = "", //beCharacter.value!!.otp0.toString(),
otp1 = "", //beCharacter.value!!.otp1.toString(),
minorVersion = beCharacter.value!!.characterCreationFirmwareVersion.minorVersion.toInt(),
majorVersion = beCharacter.value!!.characterCreationFirmwareVersion.majorVersion.toInt(),
)
storageRepository
.temporaryMonsterDao()
.insertBECharacterData(temporaryBECharacterData)
val transformationHistoryWatch = beCharacter.value!!.transformationHistory
val domainTransformationHistory = transformationHistoryWatch.map { item ->
TemporaryTransformationHistory(
monId = characterId,
toCharIndex = item.toCharIndex.toInt(),
yearsSince1988 = item.yearsSince1988.toInt(),
month = item.month.toInt(),
day = item.day.toInt()
)
}
storageRepository
.temporaryMonsterDao()
.insertTransformationHistory(*domainTransformationHistory.toTypedArray())
} }
} }
@@ -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
}
@@ -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
}
}
@@ -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"
}
@@ -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"
}
}
}
@@ -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)
}
}
}
}
}
@@ -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}")
}
}
}
@@ -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),
)
}
}
@@ -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
}
@@ -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)
}
@@ -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"
}
}
@@ -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
)
}
}
@@ -1,37 +1,262 @@
package com.github.nacabaro.vbhelper.components package com.github.nacabaro.vbhelper.components
import android.widget.Toast
import androidx.compose.foundation.Image 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.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.utils.BitmapData
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 @Composable
fun CharacterEntry( fun CharacterEntry(
icon: ImageBitmap, icon: BitmapData,
modifier: Modifier = Modifier, 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 = { } 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
Card( Card(
shape = MaterialTheme.shapes.medium, shape = shape,
onClick = onClick, onClick = when (disabled) {
true -> { {} }
false -> onClick
},
modifier = modifier modifier = modifier
.padding(8.dp) .aspectRatio(1f)
.size(96.dp) .padding(8.dp),
colors = cardColors
) { ) {
Image( Box(
bitmap = icon, contentAlignment = Alignment.BottomCenter,
contentDescription = "Icon",
filterQuality = FilterQuality.None,
modifier = Modifier modifier = Modifier
.padding(8.dp)
.fillMaxSize() .fillMaxSize()
.padding(4.dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = "Icon",
filterQuality = FilterQuality.None,
colorFilter = when (obscure) {
true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
false -> null
},
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)
)
}
}
}
}
@Composable
fun ItemDisplay(
icon: Int,
textValue: String,
modifier: Modifier = Modifier,
definition: String = "",
) {
val context = LocalContext.current
Card(
modifier = modifier,
shape = androidx.compose.material.MaterialTheme.shapes.small,
onClick = {
Toast.makeText(context, definition, Toast.LENGTH_SHORT).show()
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Icon(
painter = painterResource(icon),
contentDescription = "Vitals",
modifier = Modifier
.fillMaxSize(0.5f)
.padding(8.dp)
)
Text(
text = textValue,
textAlign = TextAlign.Center,
fontFamily = MaterialTheme.typography.titleLarge.fontFamily,
fontWeight = FontWeight.Bold,
)
}
}
}
@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,
)
}
}
}
} }
@@ -1,64 +0,0 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun StorageEntry(
name: String,
nameBitmap: ImageBitmap? = null,
modifier: Modifier = Modifier,
icon: Int? = null,
bitmap: ImageBitmap? = null,
onClick: () -> Unit = { }
) {
Card(
shape = MaterialTheme.shapes.medium,
onClick = onClick,
modifier = modifier
.padding(8.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
) {
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
} else if (icon != null) {
Image(
painter = painterResource(id = icon),
contentDescription = name,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
)
}
Text(
text = name,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
}
}
}
@@ -23,6 +23,9 @@ fun TopBanner(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onGearClick: (() -> Unit)? = null, onGearClick: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null, onBackClick: (() -> Unit)? = null,
onScanClick: (() -> Unit)? = null,
onAdventureClick: (() -> Unit)? = null,
onModifyClick: (() -> Unit)? = null
) { ) {
Box( // Use Box to overlay elements Box( // Use Box to overlay elements
modifier = modifier modifier = modifier
@@ -35,29 +38,62 @@ fun TopBanner(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 24.sp, fontSize = 24.sp,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) // Center the text .align(Alignment.Center)
) )
if (onGearClick != null) { if (onGearClick != null) {
IconButton( IconButton(
onClick = onGearClick, onClick = onGearClick,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) // Place gear icon at the end .align(Alignment.CenterEnd)
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.baseline_settings_24), // Use a gear icon painter = painterResource(R.drawable.baseline_settings_24),
contentDescription = "Settings" contentDescription = "Settings"
) )
} }
} } else if (onAdventureClick != null) {
IconButton(
onClick = onAdventureClick,
modifier = Modifier
.align(Alignment.CenterEnd)
) {
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"
)
}
}
if (onBackClick != null) { if (onScanClick != null) {
IconButton(
onClick = onScanClick,
modifier = Modifier
.align(Alignment.CenterStart)
) {
Icon(
painter = painterResource(R.drawable.baseline_nfc_24),
contentDescription = "Scan"
)
}
} else if (onBackClick != null) {
IconButton( IconButton(
onClick = onBackClick, onClick = onBackClick,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterStart) // Place gear icon at the end .align(Alignment.CenterStart)
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.baseline_arrow_back_24), // Use a gear icon painter = painterResource(R.drawable.baseline_arrow_back_24),
contentDescription = "Settings" contentDescription = "Settings"
) )
} }
@@ -0,0 +1,76 @@
package com.github.nacabaro.vbhelper.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
@Composable
fun TransformationHistoryCard(
transformationHistory: List<CharacterDtos.TransformationHistory>,
modifier: Modifier= Modifier
) {
Card (
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = modifier
) {
LazyRow (
modifier = Modifier
.padding(8.dp)
) {
items(transformationHistory) { transformation ->
TransformationHistoryItem(transformation)
}
}
}
}
@Composable
fun TransformationHistoryItem(
transformation: CharacterDtos.TransformationHistory
) {
val bitmapData = BitmapData(
bitmap = transformation.spriteIdle,
width = transformation.spriteWidth,
height = transformation.spriteHeight
)
val bitmap = remember (bitmapData) { bitmapData.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (bitmap.width * 3 / density).dp
Box (
contentAlignment = Alignment.BottomCenter,
modifier = Modifier
.aspectRatio(1f)
.fillMaxWidth()
.size((64*3/density).dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = "Transformation",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
}
}
@@ -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.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
@RewriteQueriesToDropUnusedColumns
interface AdventureDao {
@Query("""
INSERT INTO Adventure (characterId, originalDuration, finishesAdventure)
VALUES (:characterId, :originalDuration, strftime('%s', 'now') + :timeInSeconds)
""")
fun insertNewAdventure(characterId: Long, originalDuration: Long, timeInSeconds: Long)
@Query("""
SELECT COUNT(*) FROM Adventure
""")
fun getAdventureCount(): Int
@Query(
"""
SELECT
uc.*,
c.stage,
c.attribute,
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 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
"""
)
fun getAdventureCharacters(): Flow<List<CharacterDtos.AdventureCharacterWithSprites>>
@Query("""
DELETE FROM Adventure
WHERE characterId = :characterId
""")
suspend fun deleteAdventure(characterId: Long)
}
@@ -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,24 +3,88 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.Character import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.Sprites import com.github.nacabaro.vbhelper.domain.characters.Sprite
import org.w3c.dom.CharacterData import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface CharacterDao { interface CharacterDao {
@Insert @Insert
suspend fun insertCharacter(vararg characterData: Character) suspend fun insertCharacter(vararg characterData: CardCharacter)
@Query("SELECT * FROM Character") @Query("SELECT * FROM CardCharacter WHERE charaIndex = :monIndex AND cardId = :dimId LIMIT 1")
suspend fun getAllCharacters(): List<Character> fun getCharacterByMonIndex(monIndex: Int, dimId: Long): CardCharacter
@Query("SELECT * FROM Character WHERE dimId = :dimId")
suspend fun getCharacterByDimId(dimId: Int): List<Character>
@Insert @Insert
suspend fun insertSprite(vararg sprite: Sprites) suspend fun insertSprite(vararg sprite: Sprite)
@Query("SELECT * FROM Sprites") @Query(
suspend fun getAllSprites(): List<Sprites> """
SELECT
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.cardId = d.id
JOIN CardProgress cp ON d.id = cp.cardId
WHERE c.id = :charId
"""
)
suspend fun getCharacterInfo(charId: Long): CharacterDtos.CardCharacterInfo
@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)
}
@@ -0,0 +1,60 @@
package com.github.nacabaro.vbhelper.daos
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(
"""
INSERT OR IGNORE INTO Dex(id, discoveredOn)
VALUES (
(SELECT id FROM CardCharacter WHERE charaIndex = :charIndex AND cardId = :cardId),
:discoveredOn
)
"""
)
fun insertCharacter(charIndex: Int, cardId: Long, discoveredOn: Long)
@Query(
"""
SELECT
c.id AS id,
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.cardId = :cardId
"""
)
fun getSingleCardProgress(cardId: Long): Flow<List<CharacterDtos.CardCharaProgress>>
@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 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
"""
)
fun getCardsWithProgress(): Flow<List<CardDtos.CardProgress>>
}
@@ -1,16 +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.Dim
@Dao
interface DiMDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNewDim(dim: Dim)
@Query("SELECT * FROM Dim")
suspend fun getAllDims(): List<Dim>
}
@@ -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)
}
@@ -0,0 +1,54 @@
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 {
@Query(
"""
SELECT *
FROM Items
ORDER BY Items.itemIcon ASC
"""
)
fun getAllItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
@Query(
"""
SELECT *
FROM Items
WHERE quantity > 0
"""
)
fun getAllUserItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
@Query(
"""
SELECT *
FROM Items
WHERE Items.id = :itemId
"""
)
suspend fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities
@Query(
"""
UPDATE Items
SET quantity = quantity - 1
WHERE id = :itemId
"""
)
suspend fun useItem(itemId: Long)
@Query(
"""
UPDATE Items
SET quantity = quantity + :itemAmount
WHERE id = :itemId
"""
)
suspend fun purchaseItem(itemId: Long, itemAmount: Int)
}
@@ -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
@@ -0,0 +1,258 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
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
@Insert
fun insertBECharacterData(characterData: BECharacterData)
@Insert
fun insertVBCharacterData(characterData: VBCharacterData)
@Upsert
fun updateCharacter(character: UserCharacter)
@Upsert
fun updateBECharacterData(characterData: BECharacterData)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTransformationHistory(vararg transformationHistory: TransformationHistory)
@Upsert
fun insertSpecialMissions(vararg specialMissions: SpecialMissions)
@Query(
"""
SELECT
c.id AS id,
s.spriteIdle1 AS spriteIdle,
s.width AS spriteWidth,
s.height AS spriteHeight,
c.charaIndex AS monIndex,
t.transformationDate AS transformationDate
FROM TransformationHistory t
JOIN CardCharacter c ON c.id = t.stageId
JOIN Sprite s ON s.id = c.spriteId
WHERE monId = :monId
"""
)
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(
"""
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
"""
)
fun getAllCharacters(): Flow<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 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")
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(
"""
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 as isInAdventure,
uc.isActive as active
FROM UserCharacter uc
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
"""
)
fun getActiveCharacter(): Flow<CharacterDtos.CharacterWithSprites?>
@Query("DELETE FROM UserCharacter WHERE id = :id")
fun deleteCharacterById(id: Long)
@Query("UPDATE UserCharacter SET isActive = 0 WHERE isActive = 1")
fun clearActiveCharacter()
@Query("UPDATE UserCharacter SET isActive = 1 WHERE id = :id")
fun setActiveCharacter(id: Long)
@Query(
"""
SELECT c.*
FROM CardCharacter c
join UserCharacter uc on c.id = uc.charId
where uc.id = :charId
LIMIT 1
"""
)
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>
}
@@ -1,15 +0,0 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.UserMonsters
@Dao
interface UserMonstersDao {
@Insert
fun insertUserMonsters(userMonsters: UserMonsters)
@Query("SELECT * FROM UserMonsters WHERE userId = :userId")
fun getUserMonsters(userId: Int): List<UserMonsters>
}
@@ -1,11 +0,0 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory
@Dao
interface UserMonstersTransformationHistoryDao {
@Insert
fun insertTransformations(vararg transformations: TemporaryTransformationHistory)
}
@@ -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,29 +2,110 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase 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.CharacterDao
import com.github.nacabaro.vbhelper.daos.DiMDao import com.github.nacabaro.vbhelper.daos.DexDao
import com.github.nacabaro.vbhelper.domain.Character import com.github.nacabaro.vbhelper.daos.CardDao
import com.github.nacabaro.vbhelper.domain.Dim import com.github.nacabaro.vbhelper.daos.CardFusionsDao
import com.github.nacabaro.vbhelper.domain.Sprites import com.github.nacabaro.vbhelper.daos.CardProgressDao
import com.github.nacabaro.vbhelper.temporary_daos.TemporaryMonsterDao import com.github.nacabaro.vbhelper.daos.ItemDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryBECharacterData import com.github.nacabaro.vbhelper.daos.SpecialMissionDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryCharacterData import com.github.nacabaro.vbhelper.daos.SpriteDao
import com.github.nacabaro.vbhelper.temporary_domain.TemporaryTransformationHistory import com.github.nacabaro.vbhelper.daos.UserCharacterDao
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( @Database(
version = 1, version = 3,
exportSchema = false,
entities = [ entities = [
TemporaryCharacterData::class, Card::class,
TemporaryBECharacterData::class, CardProgress::class,
TemporaryTransformationHistory::class, CardCharacter::class,
Dim::class, CardAdventure::class,
Character::class, CardFusions::class,
Sprites::class Sprite::class,
UserCharacter::class,
BECharacterData::class,
VBCharacterData::class,
SpecialMissions::class,
TransformationHistory::class,
VitalsHistory::class,
Dex::class,
Items::class,
Adventure::class,
Background::class,
PossibleTransformations::class,
ValidatedCardEntity::class,
VitalWearCharacterSettings::class,
CharacterTransferPolicy::class
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun temporaryMonsterDao(): TemporaryMonsterDao abstract fun cardDao(): CardDao
abstract fun dimDao(): DiMDao abstract fun cardProgressDao(): CardProgressDao
abstract fun characterDao(): CharacterDao 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,7 +1,15 @@
package com.github.nacabaro.vbhelper.di package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase 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 { interface AppContainer {
val db: AppDatabase val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
val currencyRepository: CurrencyRepository
val validatedCardManager: ValidatedCardManager
val companionLogService: CompanionLogService
} }
@@ -1,14 +1,50 @@
import android.content.Context 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 androidx.room.Room
import com.github.nacabaro.vbhelper.database.AppDatabase import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.AppContainer 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 { class DefaultAppContainer(private val context: Context) : AppContainer {
override val db: AppDatabase by lazy { override val db: AppDatabase by lazy {
Room.databaseBuilder( Room.databaseBuilder(
context = context, context = context,
klass = AppDatabase::class.java, klass = AppDatabase::class.java,
"internalDb" "internalDb"
).build() )
.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 DefaultAppContainer
import android.app.Application import android.app.Application
import com.github.nacabaro.vbhelper.companion.validation.ValidatedCardManager
import com.github.nacabaro.vbhelper.companion.logs.CompanionLogService
class VBHelper : Application() { class VBHelper : Application() {
lateinit var container: DefaultAppContainer lateinit var container: DefaultAppContainer
val validatedCardManager: ValidatedCardManager
get() = container.validatedCardManager
val companionLogService: CompanionLogService
get() = container.companionLogService
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
container = DefaultAppContainer(applicationContext) container = DefaultAppContainer(applicationContext)
@@ -1,33 +0,0 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Dim::class,
parentColumns = ["id"],
childColumns = ["dimId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Character (
@PrimaryKey(autoGenerate = true) val id: Long,
val dimId: Int,
val monIndex: Int,
val name: ByteArray,
val stage: Int, // These should be replaced with enums
val attribute: Int, // 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
)
@@ -1,16 +0,0 @@
package com.github.nacabaro.vbhelper.domain
import android.icu.text.ListFormatter.Width
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Dim(
@PrimaryKey(autoGenerate = true)
val id: Int,
val logo: ByteArray,
val logoWidth: Int,
val logoHeight: Int,
val name: String,
val stageCount: Int
)
@@ -1,28 +0,0 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Dim::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
)
@@ -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
)
@@ -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?
)
@@ -0,0 +1,22 @@
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.device_data.UserCharacter
@Entity(
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
parentColumns = ["id"],
childColumns = ["characterId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Adventure(
@PrimaryKey val characterId: Long,
val originalDuration: Long,
val finishesAdventure: Long
)
@@ -0,0 +1,21 @@
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 = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Dex(
@PrimaryKey val id: Long = 0,
val discoveredOn: Long
)
@@ -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
}
}
@@ -1,28 +1,26 @@
package com.github.nacabaro.vbhelper.temporary_domain package com.github.nacabaro.vbhelper.domain.device_data
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.be.FirmwareVersion
import com.github.cfogrady.vbnfc.data.NfcCharacter import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = TemporaryCharacterData::class, entity = UserCharacter::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["id"], childColumns = ["id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
) )
] ]
) )
data class TemporaryBECharacterData ( data class BECharacterData (
@PrimaryKey(autoGenerate = true) val id: Long, @PrimaryKey(autoGenerate = true) val id: Long,
val trainingHp: Int, val trainingHp: Int,
val trainingAp: Int, val trainingAp: Int,
val trainingBp: Int, val trainingBp: Int,
val remainingTrainingTimeInMinutes: Int, var remainingTrainingTimeInMinutes: Int,
val itemEffectMentalStateValue: Int, val itemEffectMentalStateValue: Int,
val itemEffectMentalStateMinutesRemaining: Int, val itemEffectMentalStateMinutesRemaining: Int,
val itemEffectActivityLevelValue: Int, val itemEffectActivityLevelValue: Int,
@@ -34,9 +32,9 @@ data class TemporaryBECharacterData (
val abilityBranch: Int, val abilityBranch: Int,
val abilityReset: Int, val abilityReset: Int,
val rank: Int, val rank: Int,
val itemType: Int, var itemType: Int,
val itemMultiplier: Int, var itemMultiplier: Int,
val itemRemainingTime: Int, var itemRemainingTime: Int,
val otp0: String, val otp0: String,
val otp1: String, val otp1: String,
val minorVersion: Int, val minorVersion: Int,
@@ -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
)
@@ -0,0 +1,34 @@
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.card.CardCharacter
@Entity(
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["stageId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["monId"]),
Index(value = ["stageId"])
]
)
data class TransformationHistory (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val monId: Long,
val stageId: Long,
val transformationDate: Long
)
@@ -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
}
@@ -0,0 +1,42 @@
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.card.CardCharacter
@Entity(
foreignKeys = [
ForeignKey(
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
*/
data class UserCharacter (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
var charId: Long,
var ageInDays: Int,
var mood: Int,
var vitalPoints: Int,
var transformationCountdown: Int,
var injuryStatus: NfcCharacter.InjuryStatus,
var trophies: Int,
var currentPhaseBattlesWon: Int,
var currentPhaseBattlesLost: Int,
var totalBattlesWon: Int,
var totalBattlesLost: Int,
var activityLevel: Int,
var heartRateCurrent: Int,
var characterType: DeviceType,
var isActive: Boolean
)
@@ -0,0 +1,21 @@
package com.github.nacabaro.vbhelper.domain.device_data
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
)
@@ -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,
)

Some files were not shown because too many files have changed in this diff Show More