Compare commits
166 Commits
ccb06960a2
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
| 985c2fb730 | |||
| 0dc96a8c45 | |||
| 10bca503e5 | |||
| 1d335e9a37 | |||
| fbf119fb8a | |||
| 3d6adaad2c | |||
| c3a972571a | |||
| 4e45a547f4 | |||
| 792628d4a4 | |||
| b6b669a0db | |||
| 540a0474da | |||
| 49734d5eef | |||
| b2c3728e9a | |||
| 39cab7f799 | |||
|
|
1e007a3553 | ||
|
|
1a487f950d | ||
|
|
ea13e38402 | ||
|
|
8e2372085d | ||
|
|
129d910126 | ||
|
|
1a23e5e35a | ||
|
|
660cf633da | ||
|
|
1e7a7c68aa | ||
|
|
070fda0989 | ||
|
|
af05c41b75 | ||
|
|
efa4c2a299 | ||
|
|
2f5918b7a2 | ||
|
|
cb6f7122ed | ||
|
|
7cfc6d4f65 | ||
|
|
cdae6a8b39 | ||
|
|
9d46d1420e | ||
|
|
6d43b0635c | ||
|
|
4c138ee499 | ||
|
|
0fcfa7e782 | ||
|
|
791cf4164c | ||
|
|
fb12deaa3e | ||
|
|
6e597462ef | ||
|
|
12ffc5b15f | ||
|
|
9c81bd4905 | ||
|
|
f1cbc60857 | ||
|
|
a2798ddc82 | ||
|
|
ecf2b01723 | ||
|
|
18d6f2a31e | ||
|
|
7cbcf8d07c | ||
|
|
dc487d238a | ||
|
|
b8c702f665 | ||
|
|
259b59f519 | ||
|
|
5289bac908 | ||
|
|
34e11d97e5 | ||
|
|
ca6684a093 | ||
|
|
6e48a6f455 | ||
| b8af70406f | |||
| 7eae6f820e | |||
| 2076d53b11 | |||
| e20602cc3d | |||
| 04dc066191 | |||
| 72b3df429f | |||
| 8d94afeb55 | |||
| 8bae59ed29 | |||
| c401b7919e | |||
| 9983ba10cb | |||
| 171bc7436e | |||
| 04a30aa766 | |||
| 2356726e3f | |||
| 6151ad889c | |||
| 1f15d73573 | |||
| 40589f171d | |||
| a58c851240 | |||
| 2c6b514538 | |||
| f6a8b83343 | |||
| eb3fa5108f | |||
| 922db42322 | |||
| 71177b92f9 | |||
| 34e68443ae | |||
| 857733ec9c | |||
| 7e78ad8b7d | |||
| 430fa61be1 | |||
| 70e05cbb21 | |||
| b67073622f | |||
| c90579903d | |||
| 1a02eab07a | |||
| 00463c8092 | |||
| 153c49fe7b | |||
| 8178ec6fd7 | |||
| 2e7e40554d | |||
| 989440de83 | |||
| 3c4adacc17 | |||
| c28e1cb8c5 | |||
| 48dab535e9 | |||
| ff26cb64b7 | |||
| 28f0787020 | |||
| 71da064423 | |||
| 23bcdc237d | |||
| b56a662c9e | |||
| dcce64f91a | |||
| 595ee0c6fb | |||
| d52102d430 | |||
| 6a4abdd547 | |||
| bd3741af2d | |||
| e254adfdce | |||
| 9c973ef348 | |||
| dc9a0827d4 | |||
| b27274928e | |||
| 6bb1e419c8 | |||
| 3cda90eedd | |||
| 98a7dc5eeb | |||
| e384e29570 | |||
| 7aa6419ece | |||
| 6b953e320d | |||
| 9f56f0283a | |||
| b5f92afae9 | |||
| f58243ef6b | |||
| 886778ee78 | |||
| 0a85324734 | |||
| b374dbfe71 | |||
| 21af6e20ba | |||
| dc9066daca | |||
| e8e19d5371 | |||
| c5242b5206 | |||
| ac2e37e494 | |||
| c5d857435d | |||
| a1fab9d399 | |||
| e02e4ec399 | |||
| 8706240fb4 | |||
| e17c492baf | |||
| 2df11701e6 | |||
| c6c0e4f758 | |||
| 8c233a3bc7 | |||
| e98b72ad7d | |||
| 1fb8dc3a86 | |||
| 8b52b0c3e5 | |||
| ee065d7b39 | |||
| 59b319c27d | |||
| cdcb7a60d4 | |||
| 79106ec9f3 | |||
| bb4cbbb98b | |||
| 84b0fee261 | |||
| 0c3ab6dc39 | |||
| a694205c74 | |||
| 793987c3fb | |||
| 7d5f9c89a9 | |||
| 6da0bfe70a | |||
| a44893bca8 | |||
| 67375292a5 | |||
| 145c827417 | |||
| 8ff1cb8779 | |||
| 706b58fd2c | |||
| 15973a79f0 | |||
| 94edc44044 | |||
| 5396b7b014 | |||
| e0cc8560d1 | |||
| c627bb0873 | |||
| 6d8ec92012 | |||
| 212358e41d | |||
| 5fca394f0e | |||
| 6c914fb947 | |||
| 5a283b8dae | |||
| 407987e410 | |||
| 27a1dd7580 | |||
| 775fbf3d9b | |||
| 2a75de4bce | |||
| 72502b3d03 | |||
| 2b8a178c05 | |||
| ab306289bd | |||
| 74b0c6695f | |||
| 44d90ff5ea | |||
| 5113283550 |
29
Android/.gitignore
vendored
29
Android/.gitignore
vendored
@@ -1,20 +1,17 @@
|
|||||||
.artifacts
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
.idea/appInsightsSettings.xml
|
/local.properties
|
||||||
.idea/caches
|
/.idea/caches
|
||||||
.idea/compiler.xml
|
/.idea/libraries
|
||||||
.idea/deploymentTargetSelector.xml
|
/.idea/modules.xml
|
||||||
.idea/deviceManager.xml
|
/.idea/workspace.xml
|
||||||
.idea/gradle.xml
|
/.idea/navEditor.xml
|
||||||
.idea/markdown.xml
|
/.idea/assetWizardSettings.xml
|
||||||
.idea/migrations.xml
|
/.idea/dictionaries
|
||||||
.idea/planningMode.xml
|
|
||||||
.idea/studiobot.xml
|
|
||||||
.idea/workspace.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
*.iml
|
/app/debug
|
||||||
build
|
/app/release
|
||||||
captures
|
|
||||||
local.properties
|
|
||||||
|
|||||||
3
Android/.idea/.gitignore
generated
vendored
3
Android/.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
6
Android/.idea/AndroidProjectSystem.xml
generated
6
Android/.idea/AndroidProjectSystem.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AndroidProjectSystem">
|
|
||||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
23
Android/.idea/appInsightsSettings.xml
generated
23
Android/.idea/appInsightsSettings.xml
generated
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AppInsightsSettings">
|
|
||||||
<option name="tabSettings">
|
|
||||||
<map>
|
|
||||||
<entry key="Android vitals">
|
|
||||||
<value>
|
|
||||||
<InsightsFilterSettings>
|
|
||||||
<option name="connection">
|
|
||||||
<ConnectionSetting>
|
|
||||||
<option name="appId" value="com.planbgamestudios.downtosize" />
|
|
||||||
</ConnectionSetting>
|
|
||||||
</option>
|
|
||||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
|
||||||
<option name="timeIntervalDays" value="SEVEN_DAYS" />
|
|
||||||
<option name="visibilityType" value="ALL" />
|
|
||||||
</InsightsFilterSettings>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
Android/.idea/compiler.xml
generated
Normal file
6
Android/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="16" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
21
Android/.idea/gradle.xml
generated
Normal file
21
Android/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="Android Studio java home" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
65
Android/.idea/misc.xml
generated
65
Android/.idea/misc.xml
generated
@@ -1,57 +1,50 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="NullableNotNullManager">
|
<component name="NullableNotNullManager">
|
||||||
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
|
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
|
||||||
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
|
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
|
||||||
<option name="myOrdered" value="true" />
|
|
||||||
<option name="myNullables">
|
<option name="myNullables">
|
||||||
<value>
|
<value>
|
||||||
<list size="17">
|
<list size="14">
|
||||||
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||||
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
|
<item index="1" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||||
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
|
<item index="2" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||||
<item index="4" class="java.lang.String" itemvalue="android.annotation.Nullable" />
|
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
||||||
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
|
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
|
||||||
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
<item index="6" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||||
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
|
<item index="7" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||||
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||||
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||||
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
<item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
|
||||||
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
|
<item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
|
||||||
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
<item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
|
||||||
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
|
|
||||||
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
|
||||||
<item index="16" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="myNotNulls">
|
<option name="myNotNulls">
|
||||||
<value>
|
<value>
|
||||||
<list size="16">
|
<list size="14">
|
||||||
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||||
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
|
<item index="1" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||||
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
|
<item index="2" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||||
<item index="4" class="java.lang.String" itemvalue="android.annotation.NonNull" />
|
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||||
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
||||||
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
<item index="6" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||||
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
|
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||||
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||||
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
||||||
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
<item index="11" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
|
||||||
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
<item index="12" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
|
||||||
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
<item index="13" class="java.lang.String" itemvalue="lombok.NonNull" />
|
||||||
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
|
|
||||||
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
20
Android/.idea/runConfigurations.xml
generated
20
Android/.idea/runConfigurations.xml
generated
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
68
Android/.idea/runConfigurations/app.xml
generated
68
Android/.idea/runConfigurations/app.xml
generated
@@ -1,68 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" focusToolWindowBeforeRun="true">
|
|
||||||
<module name="MonsterCards.app" />
|
|
||||||
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
|
|
||||||
<option name="DEPLOY" value="true" />
|
|
||||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
|
||||||
<option name="DEPLOY_AS_INSTANT" value="false" />
|
|
||||||
<option name="ARTIFACT_NAME" value="" />
|
|
||||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
|
||||||
<option name="ALL_USERS" value="false" />
|
|
||||||
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
|
|
||||||
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
|
|
||||||
<option name="CLEAR_APP_STORAGE" value="false" />
|
|
||||||
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
|
||||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
|
||||||
<option name="MODE" value="default_activity" />
|
|
||||||
<option name="RESTORE_ENABLED" value="false" />
|
|
||||||
<option name="RESTORE_FILE" value="" />
|
|
||||||
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
|
|
||||||
<option name="CLEAR_LOGCAT" value="true" />
|
|
||||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
|
|
||||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
|
||||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
|
||||||
<Auto>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="true" />
|
|
||||||
</Auto>
|
|
||||||
<Hybrid>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
</Hybrid>
|
|
||||||
<Java>
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
</Java>
|
|
||||||
<Native>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
</Native>
|
|
||||||
<Profilers>
|
|
||||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="LeakCanary" />
|
|
||||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
|
||||||
</Profilers>
|
|
||||||
<option name="DEEP_LINK" value="" />
|
|
||||||
<option name="ACTIVITY" value="" />
|
|
||||||
<option name="ACTIVITY_CLASS" value="" />
|
|
||||||
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
|
|
||||||
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
|
|
||||||
<method v="2">
|
|
||||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
2
Android/.idea/vcs.xml
generated
2
Android/.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
4
Android/README.md
Normal file
4
Android/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[](https://appcenter.ms)
|
||||||
|
|
||||||
|
# MonsterCards for Android
|
||||||
|
|
||||||
@@ -11,21 +11,15 @@ if (propertiesFile.exists()) {
|
|||||||
def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
|
def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
|
||||||
def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
|
def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
|
||||||
def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
|
def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
|
||||||
def appCenterSdkVersion = '5.0.4'
|
|
||||||
def nav_version = '2.9.8'
|
|
||||||
def room_version = '2.8.4'
|
|
||||||
def rxjava_version = '3.1.8'
|
|
||||||
def rxandroid_version = '3.0.2'
|
|
||||||
def gson_version = '2.11.0'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.majinnaibu.monstercards"
|
compileSdkVersion 31
|
||||||
compileSdk 35
|
buildToolsVersion '30.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.majinnaibu.monstercards"
|
applicationId "com.majinnaibu.monstercards"
|
||||||
minSdkVersion 23
|
minSdkVersion 22
|
||||||
targetSdk 35
|
targetSdkVersion 31
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
|
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
|
||||||
@@ -56,18 +50,14 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
buildConfig true
|
|
||||||
}
|
}
|
||||||
}
|
lintOptions {
|
||||||
|
checkDependencies true
|
||||||
java {
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +69,8 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||||
implementation "androidx.navigation:navigation-fragment:$nav_version"
|
implementation "androidx.navigation:navigation-fragment:2.3.5"
|
||||||
implementation "androidx.navigation:navigation-ui:$nav_version"
|
implementation "androidx.navigation:navigation-ui:2.3.5"
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
@@ -93,18 +83,23 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
// Room DB
|
// Room DB
|
||||||
implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
|
implementation 'io.reactivex.rxjava3:rxjava:3.1.0'
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:$rxandroid_version"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:2.3.0"
|
||||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
annotationProcessor "androidx.room:room-compiler:2.3.0"
|
||||||
implementation "androidx.room:room-rxjava3:$room_version"
|
implementation "androidx.room:room-rxjava3:2.3.0"
|
||||||
//testImplementation "androidx.room:room-testing:$room_version"
|
//testImplementation "androidx.room:room-testing:2.3.0"
|
||||||
|
|
||||||
// AppCenter
|
// AppCenter
|
||||||
debugImplementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
|
debugImplementation 'com.microsoft.appcenter:appcenter-analytics:4.2.0'
|
||||||
debugImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
debugImplementation 'com.microsoft.appcenter:appcenter-crashes:4.2.0'
|
||||||
|
|
||||||
|
// Flipper
|
||||||
|
debugImplementation 'com.facebook.flipper:flipper:0.102.0'
|
||||||
|
debugImplementation "com.facebook.soloader:soloader:0.10.1"
|
||||||
|
releaseImplementation 'com.facebook.flipper:flipper-noop:0.102.0'
|
||||||
|
|
||||||
// Other 3rd Party
|
// Other 3rd Party
|
||||||
implementation 'com.atlassian.commonmark:commonmark:0.15.2'
|
implementation 'com.atlassian.commonmark:commonmark:0.17.0'
|
||||||
implementation "com.google.code.gson:gson:$gson_version"
|
implementation 'com.google.code.gson:gson:2.8.7'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,554 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 4,
|
|
||||||
"identityHash": "ca0ad042eed557daa5ff0ada5de6596c",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthScore",
|
|
||||||
"columnName": "strength_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowAdvantage",
|
|
||||||
"columnName": "strength_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowProficiency",
|
|
||||||
"columnName": "strength_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexterityScore",
|
|
||||||
"columnName": "dexterity_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowAdvantage",
|
|
||||||
"columnName": "dexterity_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowProficiency",
|
|
||||||
"columnName": "dexterity_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionScore",
|
|
||||||
"columnName": "constitution_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowAdvantage",
|
|
||||||
"columnName": "constitution_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowProficiency",
|
|
||||||
"columnName": "constitution_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceScore",
|
|
||||||
"columnName": "intelligence_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowAdvantage",
|
|
||||||
"columnName": "intelligence_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowProficiency",
|
|
||||||
"columnName": "intelligence_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomScore",
|
|
||||||
"columnName": "wisdom_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowAdvantage",
|
|
||||||
"columnName": "wisdom_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowProficiency",
|
|
||||||
"columnName": "wisdom_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaScore",
|
|
||||||
"columnName": "charisma_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowAdvantage",
|
|
||||||
"columnName": "charisma_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowProficiency",
|
|
||||||
"columnName": "charisma_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "armorType",
|
|
||||||
"columnName": "armor_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shieldBonus",
|
|
||||||
"columnName": "shield_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "naturalArmorBonus",
|
|
||||||
"columnName": "natural_armor_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "otherArmorDescription",
|
|
||||||
"columnName": "other_armor_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hitDice",
|
|
||||||
"columnName": "hit_dice",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomHP",
|
|
||||||
"columnName": "has_custom_hit_points",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customHPDescription",
|
|
||||||
"columnName": "custom_hit_points_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "walkSpeed",
|
|
||||||
"columnName": "walk_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "burrowSpeed",
|
|
||||||
"columnName": "burrow_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "climbSpeed",
|
|
||||||
"columnName": "climb_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flySpeed",
|
|
||||||
"columnName": "fly_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canHover",
|
|
||||||
"columnName": "can_hover",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "swimSpeed",
|
|
||||||
"columnName": "swim_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomSpeed",
|
|
||||||
"columnName": "has_custom_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customSpeedDescription",
|
|
||||||
"columnName": "custom_speed_description",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "challengeRating",
|
|
||||||
"columnName": "challenge_rating",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'1'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customChallengeRatingDescription",
|
|
||||||
"columnName": "custom_challenge_rating_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customProficiencyBonus",
|
|
||||||
"columnName": "custom_proficiency_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "telepathyRange",
|
|
||||||
"columnName": "telepathy_range",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "understandsButDescription",
|
|
||||||
"columnName": "understands_but_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "senses",
|
|
||||||
"columnName": "senses",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skills",
|
|
||||||
"columnName": "skills",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageImmunities",
|
|
||||||
"columnName": "damage_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageResistances",
|
|
||||||
"columnName": "damage_resistances",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageVulnerabilities",
|
|
||||||
"columnName": "damage_vulnerabilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conditionImmunities",
|
|
||||||
"columnName": "condition_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "languages",
|
|
||||||
"columnName": "languages",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "abilities",
|
|
||||||
"columnName": "abilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actions",
|
|
||||||
"columnName": "actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "reactions",
|
|
||||||
"columnName": "reactions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActions",
|
|
||||||
"columnName": "lair_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActions",
|
|
||||||
"columnName": "legendary_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActions",
|
|
||||||
"columnName": "regional_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monsters_fts",
|
|
||||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": []
|
|
||||||
},
|
|
||||||
"ftsVersion": "FTS4",
|
|
||||||
"ftsOptions": {
|
|
||||||
"tokenizer": "simple",
|
|
||||||
"tokenizerArgs": [],
|
|
||||||
"contentTable": "monsters",
|
|
||||||
"languageIdColumnName": "",
|
|
||||||
"matchInfo": "FTS4",
|
|
||||||
"notIndexedColumns": [],
|
|
||||||
"prefixSizes": [],
|
|
||||||
"preferredOrder": "ASC"
|
|
||||||
},
|
|
||||||
"contentSyncTriggers": [
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collections",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collection_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "collectionId",
|
|
||||||
"columnName": "collection_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_collection_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "collections",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ca0ad042eed557daa5ff0ada5de6596c')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,609 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 5,
|
|
||||||
"identityHash": "6d497d0f0530c0f46bed1d57492e940e",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthScore",
|
|
||||||
"columnName": "strength_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowAdvantage",
|
|
||||||
"columnName": "strength_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowProficiency",
|
|
||||||
"columnName": "strength_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexterityScore",
|
|
||||||
"columnName": "dexterity_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowAdvantage",
|
|
||||||
"columnName": "dexterity_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowProficiency",
|
|
||||||
"columnName": "dexterity_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionScore",
|
|
||||||
"columnName": "constitution_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowAdvantage",
|
|
||||||
"columnName": "constitution_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowProficiency",
|
|
||||||
"columnName": "constitution_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceScore",
|
|
||||||
"columnName": "intelligence_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowAdvantage",
|
|
||||||
"columnName": "intelligence_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowProficiency",
|
|
||||||
"columnName": "intelligence_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomScore",
|
|
||||||
"columnName": "wisdom_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowAdvantage",
|
|
||||||
"columnName": "wisdom_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowProficiency",
|
|
||||||
"columnName": "wisdom_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaScore",
|
|
||||||
"columnName": "charisma_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowAdvantage",
|
|
||||||
"columnName": "charisma_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowProficiency",
|
|
||||||
"columnName": "charisma_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "armorType",
|
|
||||||
"columnName": "armor_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shieldBonus",
|
|
||||||
"columnName": "shield_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "naturalArmorBonus",
|
|
||||||
"columnName": "natural_armor_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "otherArmorDescription",
|
|
||||||
"columnName": "other_armor_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hitDice",
|
|
||||||
"columnName": "hit_dice",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomHP",
|
|
||||||
"columnName": "has_custom_hit_points",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customHPDescription",
|
|
||||||
"columnName": "custom_hit_points_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "walkSpeed",
|
|
||||||
"columnName": "walk_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "burrowSpeed",
|
|
||||||
"columnName": "burrow_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "climbSpeed",
|
|
||||||
"columnName": "climb_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flySpeed",
|
|
||||||
"columnName": "fly_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canHover",
|
|
||||||
"columnName": "can_hover",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "swimSpeed",
|
|
||||||
"columnName": "swim_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomSpeed",
|
|
||||||
"columnName": "has_custom_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customSpeedDescription",
|
|
||||||
"columnName": "custom_speed_description",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "challengeRating",
|
|
||||||
"columnName": "challenge_rating",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'1'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customChallengeRatingDescription",
|
|
||||||
"columnName": "custom_challenge_rating_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customProficiencyBonus",
|
|
||||||
"columnName": "custom_proficiency_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "telepathyRange",
|
|
||||||
"columnName": "telepathy_range",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "understandsButDescription",
|
|
||||||
"columnName": "understands_but_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "senses",
|
|
||||||
"columnName": "senses",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skills",
|
|
||||||
"columnName": "skills",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageImmunities",
|
|
||||||
"columnName": "damage_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageResistances",
|
|
||||||
"columnName": "damage_resistances",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageVulnerabilities",
|
|
||||||
"columnName": "damage_vulnerabilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conditionImmunities",
|
|
||||||
"columnName": "condition_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "languages",
|
|
||||||
"columnName": "languages",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "abilities",
|
|
||||||
"columnName": "abilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actions",
|
|
||||||
"columnName": "actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "reactions",
|
|
||||||
"columnName": "reactions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActions",
|
|
||||||
"columnName": "lair_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActions",
|
|
||||||
"columnName": "legendary_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActions",
|
|
||||||
"columnName": "regional_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monsters_fts",
|
|
||||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": []
|
|
||||||
},
|
|
||||||
"ftsVersion": "FTS4",
|
|
||||||
"ftsOptions": {
|
|
||||||
"tokenizer": "simple",
|
|
||||||
"tokenizerArgs": [],
|
|
||||||
"contentTable": "monsters",
|
|
||||||
"languageIdColumnName": "",
|
|
||||||
"matchInfo": "FTS4",
|
|
||||||
"notIndexedColumns": [],
|
|
||||||
"prefixSizes": [],
|
|
||||||
"preferredOrder": "ASC"
|
|
||||||
},
|
|
||||||
"contentSyncTriggers": [
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collections",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collection_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "collectionId",
|
|
||||||
"columnName": "collection_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_collection_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "collections",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "dashboard_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_dashboard_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d497d0f0530c0f46bed1d57492e940e')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,663 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 6,
|
|
||||||
"identityHash": "0ab331e246de46304f847d7ed2d10124",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthScore",
|
|
||||||
"columnName": "strength_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowAdvantage",
|
|
||||||
"columnName": "strength_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowProficiency",
|
|
||||||
"columnName": "strength_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexterityScore",
|
|
||||||
"columnName": "dexterity_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowAdvantage",
|
|
||||||
"columnName": "dexterity_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowProficiency",
|
|
||||||
"columnName": "dexterity_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionScore",
|
|
||||||
"columnName": "constitution_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowAdvantage",
|
|
||||||
"columnName": "constitution_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowProficiency",
|
|
||||||
"columnName": "constitution_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceScore",
|
|
||||||
"columnName": "intelligence_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowAdvantage",
|
|
||||||
"columnName": "intelligence_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowProficiency",
|
|
||||||
"columnName": "intelligence_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomScore",
|
|
||||||
"columnName": "wisdom_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowAdvantage",
|
|
||||||
"columnName": "wisdom_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowProficiency",
|
|
||||||
"columnName": "wisdom_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaScore",
|
|
||||||
"columnName": "charisma_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowAdvantage",
|
|
||||||
"columnName": "charisma_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowProficiency",
|
|
||||||
"columnName": "charisma_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "armorType",
|
|
||||||
"columnName": "armor_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shieldBonus",
|
|
||||||
"columnName": "shield_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "naturalArmorBonus",
|
|
||||||
"columnName": "natural_armor_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "otherArmorDescription",
|
|
||||||
"columnName": "other_armor_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hitDice",
|
|
||||||
"columnName": "hit_dice",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomHP",
|
|
||||||
"columnName": "has_custom_hit_points",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customHPDescription",
|
|
||||||
"columnName": "custom_hit_points_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "walkSpeed",
|
|
||||||
"columnName": "walk_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "burrowSpeed",
|
|
||||||
"columnName": "burrow_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "climbSpeed",
|
|
||||||
"columnName": "climb_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flySpeed",
|
|
||||||
"columnName": "fly_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canHover",
|
|
||||||
"columnName": "can_hover",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "swimSpeed",
|
|
||||||
"columnName": "swim_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomSpeed",
|
|
||||||
"columnName": "has_custom_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customSpeedDescription",
|
|
||||||
"columnName": "custom_speed_description",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "challengeRating",
|
|
||||||
"columnName": "challenge_rating",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'1'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customChallengeRatingDescription",
|
|
||||||
"columnName": "custom_challenge_rating_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customProficiencyBonus",
|
|
||||||
"columnName": "custom_proficiency_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "telepathyRange",
|
|
||||||
"columnName": "telepathy_range",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "understandsButDescription",
|
|
||||||
"columnName": "understands_but_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "senses",
|
|
||||||
"columnName": "senses",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skills",
|
|
||||||
"columnName": "skills",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageImmunities",
|
|
||||||
"columnName": "damage_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageResistances",
|
|
||||||
"columnName": "damage_resistances",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageVulnerabilities",
|
|
||||||
"columnName": "damage_vulnerabilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conditionImmunities",
|
|
||||||
"columnName": "condition_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "languages",
|
|
||||||
"columnName": "languages",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "abilities",
|
|
||||||
"columnName": "abilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actions",
|
|
||||||
"columnName": "actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "reactions",
|
|
||||||
"columnName": "reactions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActions",
|
|
||||||
"columnName": "lair_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActions",
|
|
||||||
"columnName": "legendary_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActions",
|
|
||||||
"columnName": "regional_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sourceUrl",
|
|
||||||
"columnName": "source_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bonusActions",
|
|
||||||
"columnName": "bonus_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActions",
|
|
||||||
"columnName": "mythic_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActionsDescription",
|
|
||||||
"columnName": "legendary_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsDescription",
|
|
||||||
"columnName": "lair_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsEndNote",
|
|
||||||
"columnName": "lair_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsDescription",
|
|
||||||
"columnName": "regional_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsEndNote",
|
|
||||||
"columnName": "regional_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActionsDescription",
|
|
||||||
"columnName": "mythic_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monsters_fts",
|
|
||||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": []
|
|
||||||
},
|
|
||||||
"ftsVersion": "FTS4",
|
|
||||||
"ftsOptions": {
|
|
||||||
"tokenizer": "simple",
|
|
||||||
"tokenizerArgs": [],
|
|
||||||
"contentTable": "monsters",
|
|
||||||
"languageIdColumnName": "",
|
|
||||||
"matchInfo": "FTS4",
|
|
||||||
"notIndexedColumns": [],
|
|
||||||
"prefixSizes": [],
|
|
||||||
"preferredOrder": "ASC"
|
|
||||||
},
|
|
||||||
"contentSyncTriggers": [
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collections",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collection_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "collectionId",
|
|
||||||
"columnName": "collection_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_collection_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "collections",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "dashboard_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_dashboard_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ab331e246de46304f847d7ed2d10124')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,768 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 7,
|
|
||||||
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthScore",
|
|
||||||
"columnName": "strength_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowAdvantage",
|
|
||||||
"columnName": "strength_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowProficiency",
|
|
||||||
"columnName": "strength_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexterityScore",
|
|
||||||
"columnName": "dexterity_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowAdvantage",
|
|
||||||
"columnName": "dexterity_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowProficiency",
|
|
||||||
"columnName": "dexterity_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionScore",
|
|
||||||
"columnName": "constitution_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowAdvantage",
|
|
||||||
"columnName": "constitution_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowProficiency",
|
|
||||||
"columnName": "constitution_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceScore",
|
|
||||||
"columnName": "intelligence_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowAdvantage",
|
|
||||||
"columnName": "intelligence_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowProficiency",
|
|
||||||
"columnName": "intelligence_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomScore",
|
|
||||||
"columnName": "wisdom_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowAdvantage",
|
|
||||||
"columnName": "wisdom_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowProficiency",
|
|
||||||
"columnName": "wisdom_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaScore",
|
|
||||||
"columnName": "charisma_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowAdvantage",
|
|
||||||
"columnName": "charisma_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowProficiency",
|
|
||||||
"columnName": "charisma_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "armorType",
|
|
||||||
"columnName": "armor_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shieldBonus",
|
|
||||||
"columnName": "shield_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "naturalArmorBonus",
|
|
||||||
"columnName": "natural_armor_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "otherArmorDescription",
|
|
||||||
"columnName": "other_armor_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hitDice",
|
|
||||||
"columnName": "hit_dice",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomHP",
|
|
||||||
"columnName": "has_custom_hit_points",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customHPDescription",
|
|
||||||
"columnName": "custom_hit_points_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "walkSpeed",
|
|
||||||
"columnName": "walk_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "burrowSpeed",
|
|
||||||
"columnName": "burrow_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "climbSpeed",
|
|
||||||
"columnName": "climb_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flySpeed",
|
|
||||||
"columnName": "fly_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canHover",
|
|
||||||
"columnName": "can_hover",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "swimSpeed",
|
|
||||||
"columnName": "swim_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomSpeed",
|
|
||||||
"columnName": "has_custom_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customSpeedDescription",
|
|
||||||
"columnName": "custom_speed_description",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "challengeRating",
|
|
||||||
"columnName": "challenge_rating",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'1'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customChallengeRatingDescription",
|
|
||||||
"columnName": "custom_challenge_rating_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customProficiencyBonus",
|
|
||||||
"columnName": "custom_proficiency_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "telepathyRange",
|
|
||||||
"columnName": "telepathy_range",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "understandsButDescription",
|
|
||||||
"columnName": "understands_but_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "senses",
|
|
||||||
"columnName": "senses",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skills",
|
|
||||||
"columnName": "skills",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageImmunities",
|
|
||||||
"columnName": "damage_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageResistances",
|
|
||||||
"columnName": "damage_resistances",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageVulnerabilities",
|
|
||||||
"columnName": "damage_vulnerabilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conditionImmunities",
|
|
||||||
"columnName": "condition_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "languages",
|
|
||||||
"columnName": "languages",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "abilities",
|
|
||||||
"columnName": "abilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actions",
|
|
||||||
"columnName": "actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "reactions",
|
|
||||||
"columnName": "reactions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActions",
|
|
||||||
"columnName": "lair_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActions",
|
|
||||||
"columnName": "legendary_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActions",
|
|
||||||
"columnName": "regional_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sourceUrl",
|
|
||||||
"columnName": "source_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bonusActions",
|
|
||||||
"columnName": "bonus_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActions",
|
|
||||||
"columnName": "mythic_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActionsDescription",
|
|
||||||
"columnName": "legendary_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsDescription",
|
|
||||||
"columnName": "lair_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsEndNote",
|
|
||||||
"columnName": "lair_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsDescription",
|
|
||||||
"columnName": "regional_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsEndNote",
|
|
||||||
"columnName": "regional_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActionsDescription",
|
|
||||||
"columnName": "mythic_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "playerName",
|
|
||||||
"columnName": "player_name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "background",
|
|
||||||
"columnName": "background",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "personalityTraits",
|
|
||||||
"columnName": "personality_traits",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ideals",
|
|
||||||
"columnName": "ideals",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bonds",
|
|
||||||
"columnName": "bonds",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flaws",
|
|
||||||
"columnName": "flaws",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "age",
|
|
||||||
"columnName": "age",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "height",
|
|
||||||
"columnName": "height",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "weight",
|
|
||||||
"columnName": "weight",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "eyes",
|
|
||||||
"columnName": "eyes",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skin",
|
|
||||||
"columnName": "skin",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hair",
|
|
||||||
"columnName": "hair",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "appearance",
|
|
||||||
"columnName": "appearance",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "backstory",
|
|
||||||
"columnName": "backstory",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alliesAndOrganizations",
|
|
||||||
"columnName": "allies_and_organizations",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monsters_fts",
|
|
||||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": []
|
|
||||||
},
|
|
||||||
"ftsVersion": "FTS4",
|
|
||||||
"ftsOptions": {
|
|
||||||
"tokenizer": "simple",
|
|
||||||
"tokenizerArgs": [],
|
|
||||||
"contentTable": "monsters",
|
|
||||||
"languageIdColumnName": "",
|
|
||||||
"matchInfo": "FTS4",
|
|
||||||
"notIndexedColumns": [],
|
|
||||||
"prefixSizes": [],
|
|
||||||
"preferredOrder": "ASC"
|
|
||||||
},
|
|
||||||
"contentSyncTriggers": [
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collections",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collection_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "collectionId",
|
|
||||||
"columnName": "collection_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_collection_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "collections",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "dashboard_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_dashboard_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,768 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 8,
|
|
||||||
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthScore",
|
|
||||||
"columnName": "strength_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowAdvantage",
|
|
||||||
"columnName": "strength_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "strengthSavingThrowProficiency",
|
|
||||||
"columnName": "strength_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexterityScore",
|
|
||||||
"columnName": "dexterity_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowAdvantage",
|
|
||||||
"columnName": "dexterity_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dexteritySavingThrowProficiency",
|
|
||||||
"columnName": "dexterity_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionScore",
|
|
||||||
"columnName": "constitution_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowAdvantage",
|
|
||||||
"columnName": "constitution_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "constitutionSavingThrowProficiency",
|
|
||||||
"columnName": "constitution_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceScore",
|
|
||||||
"columnName": "intelligence_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowAdvantage",
|
|
||||||
"columnName": "intelligence_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "intelligenceSavingThrowProficiency",
|
|
||||||
"columnName": "intelligence_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomScore",
|
|
||||||
"columnName": "wisdom_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowAdvantage",
|
|
||||||
"columnName": "wisdom_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wisdomSavingThrowProficiency",
|
|
||||||
"columnName": "wisdom_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaScore",
|
|
||||||
"columnName": "charisma_score",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowAdvantage",
|
|
||||||
"columnName": "charisma_saving_throw_advantage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "charismaSavingThrowProficiency",
|
|
||||||
"columnName": "charisma_saving_throw_proficiency",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "armorType",
|
|
||||||
"columnName": "armor_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'none'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shieldBonus",
|
|
||||||
"columnName": "shield_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "naturalArmorBonus",
|
|
||||||
"columnName": "natural_armor_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "otherArmorDescription",
|
|
||||||
"columnName": "other_armor_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hitDice",
|
|
||||||
"columnName": "hit_dice",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomHP",
|
|
||||||
"columnName": "has_custom_hit_points",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customHPDescription",
|
|
||||||
"columnName": "custom_hit_points_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "walkSpeed",
|
|
||||||
"columnName": "walk_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "burrowSpeed",
|
|
||||||
"columnName": "burrow_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "climbSpeed",
|
|
||||||
"columnName": "climb_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flySpeed",
|
|
||||||
"columnName": "fly_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canHover",
|
|
||||||
"columnName": "can_hover",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "swimSpeed",
|
|
||||||
"columnName": "swim_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hasCustomSpeed",
|
|
||||||
"columnName": "has_custom_speed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customSpeedDescription",
|
|
||||||
"columnName": "custom_speed_description",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "challengeRating",
|
|
||||||
"columnName": "challenge_rating",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'1'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customChallengeRatingDescription",
|
|
||||||
"columnName": "custom_challenge_rating_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "customProficiencyBonus",
|
|
||||||
"columnName": "custom_proficiency_bonus",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "telepathyRange",
|
|
||||||
"columnName": "telepathy_range",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "understandsButDescription",
|
|
||||||
"columnName": "understands_but_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "senses",
|
|
||||||
"columnName": "senses",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skills",
|
|
||||||
"columnName": "skills",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageImmunities",
|
|
||||||
"columnName": "damage_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageResistances",
|
|
||||||
"columnName": "damage_resistances",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "damageVulnerabilities",
|
|
||||||
"columnName": "damage_vulnerabilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conditionImmunities",
|
|
||||||
"columnName": "condition_immunities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "languages",
|
|
||||||
"columnName": "languages",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "abilities",
|
|
||||||
"columnName": "abilities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actions",
|
|
||||||
"columnName": "actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "reactions",
|
|
||||||
"columnName": "reactions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActions",
|
|
||||||
"columnName": "lair_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActions",
|
|
||||||
"columnName": "legendary_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActions",
|
|
||||||
"columnName": "regional_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sourceUrl",
|
|
||||||
"columnName": "source_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bonusActions",
|
|
||||||
"columnName": "bonus_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActions",
|
|
||||||
"columnName": "mythic_actions",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "legendaryActionsDescription",
|
|
||||||
"columnName": "legendary_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsDescription",
|
|
||||||
"columnName": "lair_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lairActionsEndNote",
|
|
||||||
"columnName": "lair_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsDescription",
|
|
||||||
"columnName": "regional_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "regionalActionsEndNote",
|
|
||||||
"columnName": "regional_actions_end_note",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mythicActionsDescription",
|
|
||||||
"columnName": "mythic_actions_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "playerName",
|
|
||||||
"columnName": "player_name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "background",
|
|
||||||
"columnName": "background",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "personalityTraits",
|
|
||||||
"columnName": "personality_traits",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ideals",
|
|
||||||
"columnName": "ideals",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bonds",
|
|
||||||
"columnName": "bonds",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flaws",
|
|
||||||
"columnName": "flaws",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "age",
|
|
||||||
"columnName": "age",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "height",
|
|
||||||
"columnName": "height",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "weight",
|
|
||||||
"columnName": "weight",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "eyes",
|
|
||||||
"columnName": "eyes",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "skin",
|
|
||||||
"columnName": "skin",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hair",
|
|
||||||
"columnName": "hair",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "appearance",
|
|
||||||
"columnName": "appearance",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "backstory",
|
|
||||||
"columnName": "backstory",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alliesAndOrganizations",
|
|
||||||
"columnName": "allies_and_organizations",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monsters_fts",
|
|
||||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "size",
|
|
||||||
"columnName": "size",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"columnName": "type",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subtype",
|
|
||||||
"columnName": "subtype",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alignment",
|
|
||||||
"columnName": "alignment",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": []
|
|
||||||
},
|
|
||||||
"ftsVersion": "FTS4",
|
|
||||||
"ftsOptions": {
|
|
||||||
"tokenizer": "simple",
|
|
||||||
"tokenizerArgs": [],
|
|
||||||
"contentTable": "monsters",
|
|
||||||
"languageIdColumnName": "",
|
|
||||||
"matchInfo": "FTS4",
|
|
||||||
"notIndexedColumns": [],
|
|
||||||
"prefixSizes": [],
|
|
||||||
"preferredOrder": "ASC"
|
|
||||||
},
|
|
||||||
"contentSyncTriggers": [
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
|
||||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collections",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "collection_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "collectionId",
|
|
||||||
"columnName": "collection_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_collection_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_collection_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "collections",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"collection_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "dashboard_monsters",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "monsterId",
|
|
||||||
"columnName": "monster_id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ordinal",
|
|
||||||
"columnName": "ordinal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_dashboard_monsters_monster_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "monsters",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"monster_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.majinnaibu.monstercards.init;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavDestination;
|
||||||
|
|
||||||
|
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||||
|
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||||
|
import com.facebook.flipper.core.FlipperClient;
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||||
|
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||||
|
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||||
|
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
|
||||||
|
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.majinnaibu.monstercards.BuildConfig;
|
||||||
|
|
||||||
|
public class FlipperInitializer {
|
||||||
|
|
||||||
|
public static void init(Context ctx) {
|
||||||
|
SoLoader.init(ctx, false);
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(ctx)) {
|
||||||
|
final FlipperClient client = AndroidFlipperClient.getInstance(ctx);
|
||||||
|
client.addPlugin(new InspectorFlipperPlugin(ctx, DescriptorMapping.withDefaults()));
|
||||||
|
client.addPlugin(new DatabasesFlipperPlugin(ctx));
|
||||||
|
client.addPlugin(new SharedPreferencesFlipperPlugin(ctx));
|
||||||
|
client.addPlugin(NavigationFlipperPlugin.getInstance());
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendNavigationEvent(NavController controller, NavDestination destination, Bundle arguments) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String json = gson.toJson(arguments != null ? arguments : new Bundle());
|
||||||
|
NavigationFlipperPlugin.getInstance().sendNavigationEvent(String.format("%s:%s", destination.getLabel(), json), null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".MonsterCardsApplication"
|
android:name=".MonsterCardsApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@@ -14,78 +15,47 @@
|
|||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask">
|
||||||
android:exported="true">
|
|
||||||
<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 for viewing/opening content:// URIs with binary/octet-stream MIME type (.monster) -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<action android:name="android.intent.action.EDIT" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:mimeType="application/octet-stream" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Intent filter for file:// URIs ending in .monster, .monster.txt, .card, or .card.txt -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<action android:name="android.intent.action.EDIT" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="file" android:host="*" />
|
|
||||||
<data android:pathPattern=".*\\.monster" />
|
|
||||||
<data android:pathPattern=".*\\.monster\\.txt" />
|
|
||||||
<data android:pathPattern=".*\\.card" />
|
|
||||||
<data android:pathPattern=".*\\.card\\.txt" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
<data android:mimeType="application/octet-stream" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Intent filter for viewing dndbeyond.com character URLs directly in Monster Cards -->
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<action android:name="android.intent.action.PICK" />
|
||||||
|
<action android:name="android.intent.action.INSERT" />
|
||||||
|
<action android:name="android.intent.action.INSERT_OR_EDIT" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.ALTERNATIVE" />
|
||||||
|
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="http" />
|
<data
|
||||||
<data android:scheme="https" />
|
android:mimeType="text/plain"
|
||||||
<data android:host="www.dndbeyond.com" />
|
android:scheme="content" />
|
||||||
<data android:host="dndbeyond.com" />
|
<data
|
||||||
<data android:pathPrefix="/characters/" />
|
android:mimeType="application/octet-stream"
|
||||||
|
android:scheme="content" />
|
||||||
|
<data
|
||||||
|
android:mimeType="text/plain"
|
||||||
|
android:scheme="file" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<nav-graph android:value="@navigation/mobile_navigation" />
|
<nav-graph android:value="@navigation/mobile_navigation" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
<provider
|
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:exported="true" />
|
||||||
android:authorities="com.majinnaibu.monstercards.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import androidx.room.Database;
|
|||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.data.CollectionDAO;
|
|
||||||
import com.majinnaibu.monstercards.data.DashboardDAO;
|
|
||||||
import com.majinnaibu.monstercards.data.MonsterDAO;
|
import com.majinnaibu.monstercards.data.MonsterDAO;
|
||||||
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
|
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
|
||||||
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
|
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
|
||||||
@@ -14,13 +12,10 @@ import com.majinnaibu.monstercards.data.converters.SetOfLanguageConverter;
|
|||||||
import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter;
|
import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter;
|
||||||
import com.majinnaibu.monstercards.data.converters.SetOfStringConverter;
|
import com.majinnaibu.monstercards.data.converters.SetOfStringConverter;
|
||||||
import com.majinnaibu.monstercards.data.converters.UUIDConverter;
|
import com.majinnaibu.monstercards.data.converters.UUIDConverter;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.DashboardMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.MonsterFTS;
|
import com.majinnaibu.monstercards.models.MonsterFTS;
|
||||||
|
|
||||||
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 8)
|
@Database(entities = {Monster.class, MonsterFTS.class}, version = 3)
|
||||||
@TypeConverters({
|
@TypeConverters({
|
||||||
ArmorTypeConverter.class,
|
ArmorTypeConverter.class,
|
||||||
ChallengeRatingConverter.class,
|
ChallengeRatingConverter.class,
|
||||||
@@ -32,6 +27,4 @@ import com.majinnaibu.monstercards.models.MonsterFTS;
|
|||||||
})
|
})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract MonsterDAO monsterDAO();
|
public abstract MonsterDAO monsterDAO();
|
||||||
public abstract CollectionDAO collectionDAO();
|
|
||||||
public abstract DashboardDAO dashboardDAO();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.majinnaibu.monstercards;
|
package com.majinnaibu.monstercards;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.DocumentsContract;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -16,27 +17,19 @@ import androidx.navigation.fragment.NavHostFragment;
|
|||||||
import androidx.navigation.ui.AppBarConfiguration;
|
import androidx.navigation.ui.AppBarConfiguration;
|
||||||
import androidx.navigation.ui.NavigationUI;
|
import androidx.navigation.ui.NavigationUI;
|
||||||
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
|
||||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||||
|
import com.majinnaibu.monstercards.init.FlipperInitializer;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -66,6 +59,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
.build();
|
.build();
|
||||||
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
|
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
|
||||||
NavController navController = navHostFragment.getNavController();
|
NavController navController = navHostFragment.getNavController();
|
||||||
|
navController.addOnDestinationChangedListener(FlipperInitializer::sendNavigationEvent);
|
||||||
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
|
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
|
||||||
NavigationUI.setupWithNavController(navView, navController);
|
NavigationUI.setupWithNavController(navView, navController);
|
||||||
onNewIntent(getIntent());
|
onNewIntent(getIntent());
|
||||||
@@ -75,124 +69,48 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String action = intent.getAction();
|
|
||||||
Bundle extras = intent.getExtras();
|
|
||||||
|
|
||||||
if ("android.intent.action.SEND".equals(action) && extras != null) {
|
|
||||||
CharSequence sharedText = extras.getCharSequence(Intent.EXTRA_TEXT);
|
|
||||||
if (sharedText != null) {
|
|
||||||
String textStr = sharedText.toString().trim();
|
|
||||||
DnDBeyondImporter dndImporter = new DnDBeyondImporter();
|
|
||||||
if (dndImporter.canImport(textStr)) {
|
|
||||||
importMonsterFromInputAndNavigate(textStr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("android.intent.action.VIEW".equals(action)) {
|
|
||||||
Uri dataUri = intent.getData();
|
|
||||||
if (dataUri != null) {
|
|
||||||
String dataStr = dataUri.toString().trim();
|
|
||||||
DnDBeyondImporter dndImporter = new DnDBeyondImporter();
|
|
||||||
if (dndImporter.canImport(dataStr)) {
|
|
||||||
importMonsterFromInputAndNavigate(dataStr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String json = readMonsterJSONFromIntent(intent);
|
String json = readMonsterJSONFromIntent(intent);
|
||||||
if (!StringHelper.isNullOrEmpty(json)) {
|
if (!StringHelper.isNullOrEmpty(json)) {
|
||||||
importMonsterFromInputAndNavigate(json);
|
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
||||||
|
NavController navController = navHostFragment.getNavController();
|
||||||
|
NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json);
|
||||||
|
navController.navigate(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importMonsterFromInputAndNavigate(@NonNull String input) {
|
|
||||||
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
|
|
||||||
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(monster -> {
|
|
||||||
String serializedJson = new Gson().toJson(monster);
|
|
||||||
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
|
||||||
NavController navController = navHostFragment.getNavController();
|
|
||||||
NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
|
|
||||||
navController.navigate(navAction);
|
|
||||||
}, throwable -> {
|
|
||||||
Logger.logError("Failed to import monster from input", throwable);
|
|
||||||
Toast.makeText(this, R.string.failed_to_import_url, Toast.LENGTH_LONG).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
|
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
Bundle extras = intent.getExtras();
|
Bundle extras = intent.getExtras();
|
||||||
|
String type = intent.getType();
|
||||||
|
String json;
|
||||||
Uri uri = null;
|
Uri uri = null;
|
||||||
if ("android.intent.action.SEND".equals(action)) {
|
if ("android.intent.action.MAIN".equals(action)) {
|
||||||
if (extras != null) {
|
return null;
|
||||||
uri = extras.getParcelable(Intent.EXTRA_STREAM);
|
} else if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
|
||||||
}
|
uri = extras.getParcelable("android.intent.extra.STREAM");
|
||||||
} else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
|
} else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
|
||||||
uri = intent.getData();
|
uri = intent.getData();
|
||||||
} else {
|
} else {
|
||||||
Logger.logError(String.format("unexpected launch configuration action: %s", action));
|
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s", action, type));
|
||||||
}
|
}
|
||||||
|
if (uri == null) {
|
||||||
if (uri == null || !isMonsterFile(uri)) {
|
|
||||||
if (uri != null) {
|
|
||||||
Logger.logError("Ignored file because extension is not supported (.monster, .card): " + uri);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
json = readContentsOfUri(uri);
|
||||||
String json = readContentsOfUri(uri);
|
|
||||||
if (StringHelper.isNullOrEmpty(json)) {
|
if (StringHelper.isNullOrEmpty(json)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMonsterFile(@NonNull Uri uri) {
|
|
||||||
String fileName = getFileNameFromUri(uri);
|
|
||||||
if (fileName == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
|
||||||
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|
|
||||||
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getFileNameFromUri(@NonNull Uri uri) {
|
|
||||||
String displayName = null;
|
|
||||||
if ("content".equals(uri.getScheme())) {
|
|
||||||
try (Cursor cursor = getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
|
||||||
if (nameIndex != -1) {
|
|
||||||
displayName = cursor.getString(nameIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.logError("Error querying display name from content URI", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (displayName == null) {
|
|
||||||
displayName = uri.getPath();
|
|
||||||
}
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String readContentsOfUri(Uri uri) {
|
private String readContentsOfUri(Uri uri) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
try (InputStream inputStream =
|
try (
|
||||||
getContentResolver().openInputStream(uri);
|
InputStream inputStream = openInputStream(uri);
|
||||||
BufferedReader reader = new BufferedReader(
|
BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream)))
|
||||||
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
|
) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
builder.append(line);
|
builder.append(line);
|
||||||
@@ -203,4 +121,36 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isVirtualFile(Uri uri) {
|
||||||
|
if (!DocumentsContract.isDocumentUri(this, uri)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor cursor = getContentResolver().query(
|
||||||
|
uri,
|
||||||
|
new String[]{DocumentsContract.Document.COLUMN_FLAGS},
|
||||||
|
null, null, null);
|
||||||
|
|
||||||
|
int flags = 0;
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
flags = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream openInputStream(Uri uri) throws IOException {
|
||||||
|
ContentResolver resolver = getContentResolver();
|
||||||
|
if (isVirtualFile(uri)) {
|
||||||
|
String[] openableMimeTypes = resolver.getStreamTypes(uri, "*/*");
|
||||||
|
if (openableMimeTypes == null || openableMimeTypes.length <= 0) {
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
return resolver.openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null).createInputStream();
|
||||||
|
} else {
|
||||||
|
return resolver.openInputStream(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.room.migration.Migration;
|
|||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
|
import com.majinnaibu.monstercards.init.FlipperInitializer;
|
||||||
|
|
||||||
public class MonsterCardsApplication extends Application {
|
public class MonsterCardsApplication extends Application {
|
||||||
|
|
||||||
@@ -35,66 +36,6 @@ public class MonsterCardsApplication extends Application {
|
|||||||
database.execSQL("ALTER TABLE new_monsters RENAME TO monsters");
|
database.execSQL("ALTER TABLE new_monsters RENAME TO monsters");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `collections` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
|
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `collection_monsters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
|
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `collection_monsters` (`collection_id`)");
|
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `collection_monsters` (`monster_id`)");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `dashboard_monsters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
|
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `dashboard_monsters` (`monster_id`)");
|
|
||||||
database.execSQL("INSERT INTO dashboard_monsters(monster_id, ordinal) SELECT id, 0 FROM monsters");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `source_url` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonus_actions` TEXT DEFAULT '[]'");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions` TEXT DEFAULT '[]'");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `legendary_actions_description` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_description` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_end_note` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_description` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_end_note` TEXT DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions_description` TEXT DEFAULT ''");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `player_name` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `background` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `personality_traits` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `ideals` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonds` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `flaws` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `age` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `height` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `weight` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `eyes` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `skin` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `hair` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `appearance` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `backstory` TEXT NOT NULL DEFAULT ''");
|
|
||||||
database.execSQL("ALTER TABLE monsters ADD COLUMN `allies_and_organizations` TEXT NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `new_monsters` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
|
|
||||||
database.execSQL("INSERT INTO `new_monsters` SELECT id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions, source_url, bonus_actions, mythic_actions, legendary_actions_description, lair_actions_description, lair_actions_end_note, regional_actions_description, regional_actions_end_note, mythic_actions_description, COALESCE(player_name, ''), COALESCE(background, ''), COALESCE(personality_traits, ''), COALESCE(ideals, ''), COALESCE(bonds, ''), COALESCE(flaws, ''), COALESCE(age, ''), COALESCE(height, ''), COALESCE(weight, ''), COALESCE(eyes, ''), COALESCE(skin, ''), COALESCE(hair, ''), COALESCE(appearance, ''), COALESCE(backstory, ''), COALESCE(allies_and_organizations, '') FROM `monsters`");
|
|
||||||
database.execSQL("DROP TABLE `monsters`");
|
|
||||||
database.execSQL("ALTER TABLE `new_monsters` RENAME TO `monsters`");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private MonsterRepository m_monsterLibraryRepository;
|
private MonsterRepository m_monsterLibraryRepository;
|
||||||
|
|
||||||
|
|
||||||
@@ -112,15 +53,12 @@ public class MonsterCardsApplication extends Application {
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
// Required initialization logic here!
|
// Required initialization logic here!
|
||||||
|
|
||||||
|
FlipperInitializer.init(this);
|
||||||
|
|
||||||
// .fallbackToDestructiveMigration()
|
// .fallbackToDestructiveMigration()
|
||||||
AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters")
|
AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters")
|
||||||
.addMigrations(MIGRATION_1_2)
|
.addMigrations(MIGRATION_1_2)
|
||||||
.addMigrations(MIGRATION_2_3)
|
.addMigrations(MIGRATION_2_3)
|
||||||
.addMigrations(MIGRATION_3_4)
|
|
||||||
.addMigrations(MIGRATION_4_5)
|
|
||||||
.addMigrations(MIGRATION_5_6)
|
|
||||||
.addMigrations(MIGRATION_6_7)
|
|
||||||
.addMigrations(MIGRATION_7_8)
|
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
// .fallbackToDestructiveMigration()
|
// .fallbackToDestructiveMigration()
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.data;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
|
||||||
import androidx.room.Delete;
|
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Query;
|
|
||||||
import androidx.room.Update;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public interface CollectionDAO {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM collections")
|
|
||||||
Flowable<List<Collection>> getAll();
|
|
||||||
|
|
||||||
@Query("SELECT collections.*, COUNT(collection_monsters.id) as monsterCount FROM collections LEFT JOIN collection_monsters ON collections.id = collection_monsters.collection_id GROUP BY collections.id")
|
|
||||||
Flowable<List<CollectionWithCount>> getCollectionsWithCount();
|
|
||||||
|
|
||||||
@Query("SELECT * FROM collections WHERE id = :id LIMIT 1")
|
|
||||||
Flowable<Collection> getById(String id);
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
Completable save(Collection... collections);
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
Completable delete(Collection collection);
|
|
||||||
|
|
||||||
@Query("SELECT monsters.* FROM monsters INNER JOIN collection_monsters ON monsters.id = collection_monsters.monster_id WHERE collection_monsters.collection_id = :collectionId ORDER BY collection_monsters.ordinal ASC, collection_monsters.id ASC")
|
|
||||||
Flowable<List<Monster>> getMonstersForCollection(String collectionId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM collection_monsters WHERE collection_id = :collectionId ORDER BY ordinal ASC, id ASC")
|
|
||||||
Flowable<List<CollectionMonster>> getCollectionMonstersForCollection(String collectionId);
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
Completable addMonsterToCollection(CollectionMonster... collectionMonsters);
|
|
||||||
|
|
||||||
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
|
|
||||||
Completable removeMonsterFromCollection(String collectionId, String monsterId);
|
|
||||||
|
|
||||||
@Query("DELETE FROM collection_monsters WHERE id = :id")
|
|
||||||
Completable removeCollectionMonsterById(long id);
|
|
||||||
|
|
||||||
@Update
|
|
||||||
Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.data;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.Query;
|
|
||||||
import androidx.room.Update;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.models.DashboardMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public interface DashboardDAO {
|
|
||||||
|
|
||||||
@Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC")
|
|
||||||
Flowable<List<Monster>> getDashboardMonsters();
|
|
||||||
|
|
||||||
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
|
|
||||||
Flowable<List<DashboardMonster>> getDashboardMonsterEntries();
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
Completable addMonsterToDashboard(DashboardMonster... dashboardMonsters);
|
|
||||||
|
|
||||||
@Query("DELETE FROM dashboard_monsters WHERE id = :id")
|
|
||||||
Completable removeDashboardMonsterById(long id);
|
|
||||||
|
|
||||||
@Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId")
|
|
||||||
Completable removeMonsterFromDashboard(String monsterId);
|
|
||||||
|
|
||||||
@Query("DELETE FROM dashboard_monsters")
|
|
||||||
Completable clearDashboard();
|
|
||||||
|
|
||||||
@Update
|
|
||||||
Completable updateDashboardMonsters(List<DashboardMonster> dashboardMonsters);
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.majinnaibu.monstercards.AppDatabase;
|
import com.majinnaibu.monstercards.AppDatabase;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
|
||||||
import com.majinnaibu.monstercards.models.DashboardMonster;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -52,43 +47,6 @@ public class MonsterRepository {
|
|||||||
.observeOn(AndroidSchedulers.mainThread());
|
.observeOn(AndroidSchedulers.mainThread());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<SearchResultItem>> searchAll(String searchText) {
|
|
||||||
Flowable<List<Monster>> monstersFlowable = m_db.monsterDAO()
|
|
||||||
.getAll()
|
|
||||||
.map(monsters -> {
|
|
||||||
ArrayList<Monster> filteredMonsters = new ArrayList<>();
|
|
||||||
for (Monster monster : monsters) {
|
|
||||||
if (Helpers.monsterMatchesSearch(monster, searchText)) {
|
|
||||||
filteredMonsters.add(monster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Monster>) filteredMonsters;
|
|
||||||
});
|
|
||||||
|
|
||||||
Flowable<List<Collection>> collectionsFlowable = m_db.collectionDAO()
|
|
||||||
.getAll()
|
|
||||||
.map(collections -> {
|
|
||||||
ArrayList<Collection> filteredCollections = new ArrayList<>();
|
|
||||||
for (Collection collection : collections) {
|
|
||||||
if (StringHelper.isNullOrEmpty(searchText) || StringHelper.containsCaseInsensitive(collection.name, searchText)) {
|
|
||||||
filteredCollections.add(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Collection>) filteredCollections;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Flowable.combineLatest(monstersFlowable, collectionsFlowable, (monsters, collections) -> {
|
|
||||||
ArrayList<SearchResultItem> results = new ArrayList<>();
|
|
||||||
for (Monster monster : monsters) {
|
|
||||||
results.add(new SearchResultItem(monster));
|
|
||||||
}
|
|
||||||
for (Collection collection : collections) {
|
|
||||||
results.add(new SearchResultItem(collection));
|
|
||||||
}
|
|
||||||
return (List<SearchResultItem>) results;
|
|
||||||
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<Monster> getMonster(@NonNull UUID monsterId) {
|
public Flowable<Monster> getMonster(@NonNull UUID monsterId) {
|
||||||
return m_db.monsterDAO()
|
return m_db.monsterDAO()
|
||||||
.loadAllByIds(new String[]{monsterId.toString()})
|
.loadAllByIds(new String[]{monsterId.toString()})
|
||||||
@@ -122,144 +80,6 @@ public class MonsterRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<Collection>> getCollections() {
|
|
||||||
return m_db.collectionDAO()
|
|
||||||
.getAll()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<CollectionWithCount>> getCollectionsWithCount() {
|
|
||||||
return m_db.collectionDAO()
|
|
||||||
.getCollectionsWithCount()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<Collection> getCollection(@NonNull UUID collectionId) {
|
|
||||||
return m_db.collectionDAO()
|
|
||||||
.getById(collectionId.toString())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable saveCollection(Collection collection) {
|
|
||||||
Completable result = m_db.collectionDAO().save(collection);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable deleteCollection(Collection collection) {
|
|
||||||
Completable result = m_db.collectionDAO().delete(collection);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<Monster>> getMonstersForCollection(@NonNull UUID collectionId) {
|
|
||||||
return m_db.collectionDAO()
|
|
||||||
.getMonstersForCollection(collectionId.toString())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<CollectionMonster>> getCollectionMonstersForCollection(@NonNull UUID collectionId) {
|
|
||||||
return m_db.collectionDAO()
|
|
||||||
.getCollectionMonstersForCollection(collectionId.toString())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable addMonsterToCollection(@NonNull UUID collectionId, @NonNull UUID monsterId) {
|
|
||||||
return addMonsterToCollection(collectionId, monsterId, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable addMonsterToCollection(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
|
|
||||||
CollectionMonster collectionMonster = new CollectionMonster(collectionId, monsterId, ordinal);
|
|
||||||
Completable result = m_db.collectionDAO().addMonsterToCollection(collectionMonster);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable removeMonsterFromCollection(@NonNull UUID collectionId, @NonNull UUID monsterId) {
|
|
||||||
Completable result = m_db.collectionDAO().removeMonsterFromCollection(collectionId.toString(), monsterId.toString());
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable removeCollectionMonsterById(long junctionId) {
|
|
||||||
Completable result = m_db.collectionDAO().removeCollectionMonsterById(junctionId);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters) {
|
|
||||||
Completable result = m_db.collectionDAO().updateCollectionMonsters(collectionMonsters);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<Monster>> getDashboardMonsters() {
|
|
||||||
return m_db.dashboardDAO()
|
|
||||||
.getDashboardMonsters()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() {
|
|
||||||
return m_db.dashboardDAO()
|
|
||||||
.getDashboardMonsterEntries()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable addMonsterToDashboard(@NonNull UUID monsterId) {
|
|
||||||
DashboardMonster entry = new DashboardMonster(monsterId, 0);
|
|
||||||
Completable result = m_db.dashboardDAO().addMonsterToDashboard(entry);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable addCollectionToDashboard(@NonNull UUID collectionId) {
|
|
||||||
return m_db.collectionDAO().getMonstersForCollection(collectionId.toString())
|
|
||||||
.firstOrError()
|
|
||||||
.flatMapCompletable(monsters -> {
|
|
||||||
if (monsters.isEmpty()) {
|
|
||||||
return Completable.complete();
|
|
||||||
}
|
|
||||||
DashboardMonster[] entries = new DashboardMonster[monsters.size()];
|
|
||||||
for (int i = 0; i < monsters.size(); i++) {
|
|
||||||
entries[i] = new DashboardMonster(monsters.get(i).id, i);
|
|
||||||
}
|
|
||||||
return m_db.dashboardDAO().addMonsterToDashboard(entries);
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable removeMonsterFromDashboard(@NonNull UUID monsterId) {
|
|
||||||
Completable result = m_db.dashboardDAO().removeMonsterFromDashboard(monsterId.toString());
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable removeDashboardMonsterById(long id) {
|
|
||||||
Completable result = m_db.dashboardDAO().removeDashboardMonsterById(id);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable clearDashboard() {
|
|
||||||
Completable result = m_db.dashboardDAO().clearDashboard();
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable updateDashboardMonsters(List<DashboardMonster> items) {
|
|
||||||
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
|
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Helpers {
|
private static class Helpers {
|
||||||
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
||||||
if (StringHelper.isNullOrEmpty(searchText)) {
|
if (StringHelper.isNullOrEmpty(searchText)) {
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.exporters;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonNull;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ArmorType;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
|
||||||
import com.majinnaibu.monstercards.models.Language;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.models.Skill;
|
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class Open5eExporter {
|
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String exportMonster(@NonNull Monster monster) {
|
|
||||||
JsonObject root = new JsonObject();
|
|
||||||
root.addProperty("$schema", "../../../schema/entity.json");
|
|
||||||
root.addProperty("uuid", monster.id != null ? monster.id.toString() : UUID.randomUUID().toString());
|
|
||||||
root.addProperty("ruleset_id", "open5e");
|
|
||||||
root.addProperty("entity_type", "character");
|
|
||||||
root.addProperty("display_name", monster.name != null && !monster.name.isEmpty() ? monster.name : "Unnamed Entity");
|
|
||||||
root.addProperty("description", buildDescription(monster));
|
|
||||||
root.addProperty("version", "1.0.0");
|
|
||||||
root.addProperty("template_id", "stat_block");
|
|
||||||
|
|
||||||
JsonObject props = new JsonObject();
|
|
||||||
props.addProperty("name", monster.name != null ? monster.name : "");
|
|
||||||
props.addProperty("size", monster.size != null ? monster.size : "");
|
|
||||||
props.addProperty("type", monster.type != null ? monster.type : "");
|
|
||||||
props.addProperty("subtype", monster.subtype != null ? monster.subtype : "");
|
|
||||||
props.addProperty("alignment", monster.alignment != null ? monster.alignment : "");
|
|
||||||
props.addProperty("armor_class", monster.getArmorClassValue());
|
|
||||||
props.addProperty("armor_description", monster.getArmorClass());
|
|
||||||
|
|
||||||
if (monster.armorType != null && !monster.armorType.equals(ArmorType.NONE)) {
|
|
||||||
JsonObject armorObj = new JsonObject();
|
|
||||||
armorObj.addProperty("id", monster.armorType.stringValue);
|
|
||||||
armorObj.addProperty("name", monster.armorType.displayName);
|
|
||||||
armorObj.addProperty("armor_category", getArmorCategory(monster.armorType));
|
|
||||||
armorObj.addProperty("ac_base", monster.armorType.baseArmorClass);
|
|
||||||
|
|
||||||
Integer dexCap = getDexCap(monster.armorType);
|
|
||||||
if (dexCap != null) {
|
|
||||||
armorObj.addProperty("dex_cap", dexCap);
|
|
||||||
} else {
|
|
||||||
armorObj.add("dex_cap", JsonNull.INSTANCE);
|
|
||||||
}
|
|
||||||
armorObj.addProperty("stealth_disadvantage", isStealthDisadvantage(monster.armorType));
|
|
||||||
props.add("equipped_armor", armorObj);
|
|
||||||
} else {
|
|
||||||
props.add("equipped_armor", JsonNull.INSTANCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monster.shieldBonus > 0) {
|
|
||||||
JsonObject shieldObj = new JsonObject();
|
|
||||||
shieldObj.addProperty("id", "shield");
|
|
||||||
shieldObj.addProperty("name", "Shield");
|
|
||||||
shieldObj.addProperty("ac_bonus", monster.shieldBonus);
|
|
||||||
props.add("equipped_shield", shieldObj);
|
|
||||||
} else {
|
|
||||||
props.add("equipped_shield", JsonNull.INSTANCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject hpObj = new JsonObject();
|
|
||||||
hpObj.addProperty("max", monster.getHitPointsValue());
|
|
||||||
hpObj.addProperty("current", monster.getHitPointsValue());
|
|
||||||
hpObj.addProperty("formula", monster.hasCustomHP && !monster.customHPDescription.isEmpty() ? monster.customHPDescription : monster.getHitPoints());
|
|
||||||
hpObj.addProperty("hit_dice", monster.hitDice + "d8");
|
|
||||||
props.add("hit_points", hpObj);
|
|
||||||
|
|
||||||
props.addProperty("speed", monster.walkSpeed);
|
|
||||||
props.addProperty("speed_desc", monster.getSpeedText());
|
|
||||||
props.addProperty("challenge_rating", monster.getChallengeRatingDescription());
|
|
||||||
props.addProperty("cr", monster.challengeRating != null ? monster.challengeRating.stringValue : "1");
|
|
||||||
|
|
||||||
JsonObject abilitiesObj = new JsonObject();
|
|
||||||
abilitiesObj.addProperty("strength", monster.strengthScore);
|
|
||||||
abilitiesObj.addProperty("dexterity", monster.dexterityScore);
|
|
||||||
abilitiesObj.addProperty("constitution", monster.constitutionScore);
|
|
||||||
abilitiesObj.addProperty("intelligence", monster.intelligenceScore);
|
|
||||||
abilitiesObj.addProperty("wisdom", monster.wisdomScore);
|
|
||||||
abilitiesObj.addProperty("charisma", monster.charismaScore);
|
|
||||||
props.add("abilities", abilitiesObj);
|
|
||||||
|
|
||||||
if (monster.playerName != null && !monster.playerName.isEmpty()) props.addProperty("player_name", monster.playerName);
|
|
||||||
if (monster.background != null && !monster.background.isEmpty()) props.addProperty("background", monster.background);
|
|
||||||
if (monster.personalityTraits != null && !monster.personalityTraits.isEmpty()) props.addProperty("personality_traits", monster.personalityTraits);
|
|
||||||
if (monster.ideals != null && !monster.ideals.isEmpty()) props.addProperty("ideals", monster.ideals);
|
|
||||||
if (monster.bonds != null && !monster.bonds.isEmpty()) props.addProperty("bonds", monster.bonds);
|
|
||||||
if (monster.flaws != null && !monster.flaws.isEmpty()) props.addProperty("flaws", monster.flaws);
|
|
||||||
|
|
||||||
if (hasDetails(monster)) {
|
|
||||||
JsonObject detailsObj = new JsonObject();
|
|
||||||
if (monster.age != null && !monster.age.isEmpty()) detailsObj.addProperty("age", monster.age);
|
|
||||||
if (monster.height != null && !monster.height.isEmpty()) detailsObj.addProperty("height", monster.height);
|
|
||||||
if (monster.weight != null && !monster.weight.isEmpty()) detailsObj.addProperty("weight", monster.weight);
|
|
||||||
if (monster.eyes != null && !monster.eyes.isEmpty()) detailsObj.addProperty("eyes", monster.eyes);
|
|
||||||
if (monster.skin != null && !monster.skin.isEmpty()) detailsObj.addProperty("skin", monster.skin);
|
|
||||||
if (monster.hair != null && !monster.hair.isEmpty()) detailsObj.addProperty("hair", monster.hair);
|
|
||||||
if (monster.appearance != null && !monster.appearance.isEmpty()) detailsObj.addProperty("appearance", monster.appearance);
|
|
||||||
if (monster.backstory != null && !monster.backstory.isEmpty()) detailsObj.addProperty("backstory", monster.backstory);
|
|
||||||
if (monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty()) detailsObj.addProperty("allies_and_organizations", monster.alliesAndOrganizations);
|
|
||||||
props.add("character_details", detailsObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonArray savesArr = buildSavingThrowsArray(monster);
|
|
||||||
if (savesArr.size() > 0) props.add("saving_throws", savesArr);
|
|
||||||
|
|
||||||
JsonArray skillsArr = buildSkillsArray(monster);
|
|
||||||
if (skillsArr.size() > 0) props.add("skills", skillsArr);
|
|
||||||
|
|
||||||
if (monster.damageImmunities != null && !monster.damageImmunities.isEmpty()) props.add("damage_immunities", toJsonArray(monster.damageImmunities));
|
|
||||||
if (monster.damageResistances != null && !monster.damageResistances.isEmpty()) props.add("damage_resistances", toJsonArray(monster.damageResistances));
|
|
||||||
if (monster.damageVulnerabilities != null && !monster.damageVulnerabilities.isEmpty()) props.add("damage_vulnerabilities", toJsonArray(monster.damageVulnerabilities));
|
|
||||||
if (monster.conditionImmunities != null && !monster.conditionImmunities.isEmpty()) props.add("condition_immunities", toJsonArray(monster.conditionImmunities));
|
|
||||||
|
|
||||||
props.addProperty("senses", monster.getSensesDescription());
|
|
||||||
|
|
||||||
if (monster.languages != null && !monster.languages.isEmpty()) {
|
|
||||||
JsonArray langArr = new JsonArray();
|
|
||||||
for (Language l : monster.languages) {
|
|
||||||
if (l != null && l.getName() != null && !l.getName().isEmpty()) {
|
|
||||||
langArr.add(l.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
props.add("languages", langArr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monster.abilities != null && !monster.abilities.isEmpty()) props.add("traits", traitsToJsonArray(monster.abilities));
|
|
||||||
if (monster.actions != null && !monster.actions.isEmpty()) props.add("actions", traitsToJsonArray(monster.actions));
|
|
||||||
if (monster.bonusActions != null && !monster.bonusActions.isEmpty()) props.add("bonus_actions", traitsToJsonArray(monster.bonusActions));
|
|
||||||
if (monster.reactions != null && !monster.reactions.isEmpty()) props.add("reactions", traitsToJsonArray(monster.reactions));
|
|
||||||
if (monster.legendaryActions != null && !monster.legendaryActions.isEmpty()) props.add("legendary_actions", traitsToJsonArray(monster.legendaryActions));
|
|
||||||
if (monster.legendaryActionsDescription != null && !monster.legendaryActionsDescription.isEmpty()) props.addProperty("legendary_desc", monster.legendaryActionsDescription);
|
|
||||||
if (monster.lairActions != null && !monster.lairActions.isEmpty()) props.add("lair_actions", traitsToJsonArray(monster.lairActions));
|
|
||||||
if (monster.lairActionsDescription != null && !monster.lairActionsDescription.isEmpty()) props.addProperty("lair_desc", monster.lairActionsDescription);
|
|
||||||
if (monster.regionalActions != null && !monster.regionalActions.isEmpty()) props.add("regional_effects", traitsToJsonArray(monster.regionalActions));
|
|
||||||
if (monster.regionalActionsDescription != null && !monster.regionalActionsDescription.isEmpty()) props.addProperty("regional_desc", monster.regionalActionsDescription);
|
|
||||||
if (monster.mythicActions != null && !monster.mythicActions.isEmpty()) props.add("mythic_actions", traitsToJsonArray(monster.mythicActions));
|
|
||||||
if (monster.mythicActionsDescription != null && !monster.mythicActionsDescription.isEmpty()) props.addProperty("mythic_desc", monster.mythicActionsDescription);
|
|
||||||
|
|
||||||
if (monster.sourceUrl != null && !monster.sourceUrl.isEmpty()) props.addProperty("source_url", monster.sourceUrl);
|
|
||||||
|
|
||||||
root.add("properties", props);
|
|
||||||
return GSON.toJson(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getArmorCategory(ArmorType armorType) {
|
|
||||||
switch (armorType) {
|
|
||||||
case PADDED:
|
|
||||||
case LEATHER:
|
|
||||||
case STUDDED_LEATHER:
|
|
||||||
return "light";
|
|
||||||
case HIDE:
|
|
||||||
case CHAIN_SHIRT:
|
|
||||||
case SCALE_MAIL:
|
|
||||||
case BREASTPLATE:
|
|
||||||
case HALF_PLATE:
|
|
||||||
return "medium";
|
|
||||||
case RING_MAIL:
|
|
||||||
case CHAIN_MAIL:
|
|
||||||
case SPLINT_MAIL:
|
|
||||||
case PLATE_MAIL:
|
|
||||||
return "heavy";
|
|
||||||
default:
|
|
||||||
return "natural";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Integer getDexCap(ArmorType armorType) {
|
|
||||||
switch (armorType) {
|
|
||||||
case HIDE:
|
|
||||||
case CHAIN_SHIRT:
|
|
||||||
case SCALE_MAIL:
|
|
||||||
case BREASTPLATE:
|
|
||||||
case HALF_PLATE:
|
|
||||||
return 2;
|
|
||||||
case RING_MAIL:
|
|
||||||
case CHAIN_MAIL:
|
|
||||||
case SPLINT_MAIL:
|
|
||||||
case PLATE_MAIL:
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isStealthDisadvantage(ArmorType armorType) {
|
|
||||||
switch (armorType) {
|
|
||||||
case PADDED:
|
|
||||||
case SCALE_MAIL:
|
|
||||||
case HALF_PLATE:
|
|
||||||
case RING_MAIL:
|
|
||||||
case CHAIN_MAIL:
|
|
||||||
case SPLINT_MAIL:
|
|
||||||
case PLATE_MAIL:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDetails(Monster monster) {
|
|
||||||
return (monster.age != null && !monster.age.isEmpty()) ||
|
|
||||||
(monster.height != null && !monster.height.isEmpty()) ||
|
|
||||||
(monster.weight != null && !monster.weight.isEmpty()) ||
|
|
||||||
(monster.eyes != null && !monster.eyes.isEmpty()) ||
|
|
||||||
(monster.skin != null && !monster.skin.isEmpty()) ||
|
|
||||||
(monster.hair != null && !monster.hair.isEmpty()) ||
|
|
||||||
(monster.appearance != null && !monster.appearance.isEmpty()) ||
|
|
||||||
(monster.backstory != null && !monster.backstory.isEmpty()) ||
|
|
||||||
(monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildDescription(Monster monster) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
if (monster.size != null && !monster.size.isEmpty()) sb.append(monster.size).append(" ");
|
|
||||||
if (monster.type != null && !monster.type.isEmpty()) sb.append(monster.type);
|
|
||||||
if (monster.subtype != null && !monster.subtype.isEmpty()) sb.append(" (").append(monster.subtype).append(")");
|
|
||||||
if (monster.alignment != null && !monster.alignment.isEmpty()) sb.append(", ").append(monster.alignment);
|
|
||||||
return sb.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonArray buildSavingThrowsArray(Monster monster) {
|
|
||||||
JsonArray arr = new JsonArray();
|
|
||||||
checkSave(arr, "Strength", monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH), monster.getAbilityModifier(AbilityScore.STRENGTH) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH)));
|
|
||||||
checkSave(arr, "Dexterity", monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY), monster.getAbilityModifier(AbilityScore.DEXTERITY) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY)));
|
|
||||||
checkSave(arr, "Constitution", monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION), monster.getAbilityModifier(AbilityScore.CONSTITUTION) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION)));
|
|
||||||
checkSave(arr, "Intelligence", monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE), monster.getAbilityModifier(AbilityScore.INTELLIGENCE) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE)));
|
|
||||||
checkSave(arr, "Wisdom", monster.getSavingThrowProficiencyType(AbilityScore.WISDOM), monster.getAbilityModifier(AbilityScore.WISDOM) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.WISDOM)));
|
|
||||||
checkSave(arr, "Charisma", monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA), monster.getAbilityModifier(AbilityScore.CHARISMA) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA)));
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkSave(JsonArray arr, String statName, ProficiencyType prof, int bonus) {
|
|
||||||
if (prof != null && !prof.equals(ProficiencyType.NONE)) {
|
|
||||||
arr.add(String.format(Locale.ROOT, "%s %+d", statName, bonus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonArray buildSkillsArray(Monster monster) {
|
|
||||||
JsonArray arr = new JsonArray();
|
|
||||||
if (monster.skills != null) {
|
|
||||||
for (Skill s : monster.skills) {
|
|
||||||
if (s != null && s.proficiencyType != null && !s.proficiencyType.equals(ProficiencyType.NONE)) {
|
|
||||||
int bonus = s.getSkillBonus(monster);
|
|
||||||
arr.add(String.format(Locale.ROOT, "%s %+d", s.name.toLowerCase(Locale.ROOT), bonus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonArray toJsonArray(Collection<String> items) {
|
|
||||||
JsonArray arr = new JsonArray();
|
|
||||||
for (String s : items) {
|
|
||||||
if (s != null && !s.isEmpty()) arr.add(s);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonArray traitsToJsonArray(List<Trait> traits) {
|
|
||||||
JsonArray arr = new JsonArray();
|
|
||||||
for (Trait t : traits) {
|
|
||||||
if (t != null && t.name != null && !t.name.isEmpty()) {
|
|
||||||
JsonObject obj = new JsonObject();
|
|
||||||
obj.addProperty("name", t.name);
|
|
||||||
obj.addProperty("desc", t.description != null ? t.description : "");
|
|
||||||
arr.add(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,39 +2,310 @@ package com.majinnaibu.monstercards.helpers;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
import com.google.gson.JsonArray;
|
||||||
import com.majinnaibu.monstercards.importers.EntityImporter;
|
import com.google.gson.JsonElement;
|
||||||
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
|
import com.google.gson.JsonObject;
|
||||||
import com.majinnaibu.monstercards.importers.Open5eImporter;
|
import com.google.gson.JsonParser;
|
||||||
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
|
||||||
|
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
||||||
|
import com.majinnaibu.monstercards.models.Language;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
import com.majinnaibu.monstercards.models.Skill;
|
||||||
|
import com.majinnaibu.monstercards.models.Trait;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class MonsterImportHelper {
|
public class MonsterImportHelper {
|
||||||
private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
IMPORTERS.add(new MonsterJsonImporter());
|
|
||||||
IMPORTERS.add(new Open5eImporter());
|
|
||||||
IMPORTERS.add(new TetraCubeMonsterImporter());
|
|
||||||
IMPORTERS.add(new DnDBeyondImporter());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Monster fromJSON(String json) {
|
public static Monster fromJSON(String json) {
|
||||||
if (json != null) {
|
JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject();
|
||||||
for (EntityImporter<Monster> importer : IMPORTERS) {
|
|
||||||
if (importer.canImport(json)) {
|
Monster monster = new Monster();
|
||||||
try {
|
monster.name = Helpers.getString(rootDict, "name");
|
||||||
return importer.parse(json);
|
monster.size = Helpers.getString(rootDict, "size");
|
||||||
} catch (Exception e) {
|
monster.type = Helpers.getString(rootDict, "type");
|
||||||
throw new IllegalArgumentException("Failed to parse monster JSON", e);
|
monster.subtype = Helpers.getString(rootDict, "tag");
|
||||||
|
monster.alignment = Helpers.getString(rootDict, "alignment");
|
||||||
|
monster.hitDice = Helpers.getInt(rootDict, "hitDice");
|
||||||
|
monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(Helpers.getString(rootDict, "armorName"));
|
||||||
|
monster.shieldBonus = Helpers.getInt(rootDict, "shieldBonus");
|
||||||
|
monster.naturalArmorBonus = Helpers.getInt(rootDict, "natArmorBonus");
|
||||||
|
monster.otherArmorDescription = Helpers.getString(rootDict, "otherArmorDesc");
|
||||||
|
monster.walkSpeed = Helpers.getInt(rootDict, "speed");
|
||||||
|
monster.burrowSpeed = Helpers.getInt(rootDict, "burrowSpeed");
|
||||||
|
monster.climbSpeed = Helpers.getInt(rootDict, "climbSpeed");
|
||||||
|
monster.flySpeed = Helpers.getInt(rootDict, "flySpeed");
|
||||||
|
monster.canHover = Helpers.getBool(rootDict, "hover");
|
||||||
|
monster.swimSpeed = Helpers.getInt(rootDict, "swimSpeed");
|
||||||
|
monster.hasCustomHP = Helpers.getBool(rootDict, "customHP");
|
||||||
|
monster.hasCustomSpeed = Helpers.getBool(rootDict, "customSpeed");
|
||||||
|
monster.customHPDescription = Helpers.getString(rootDict, "hpText");
|
||||||
|
monster.customSpeedDescription = Helpers.getString(rootDict, "speedDesc");
|
||||||
|
monster.strengthScore = Helpers.getInt(rootDict, "strPoints");
|
||||||
|
monster.dexterityScore = Helpers.getInt(rootDict, "dexPoints");
|
||||||
|
monster.constitutionScore = Helpers.getInt(rootDict, "conPoints");
|
||||||
|
monster.intelligenceScore = Helpers.getInt(rootDict, "intPoints");
|
||||||
|
monster.wisdomScore = Helpers.getInt(rootDict, "wisPoints");
|
||||||
|
monster.charismaScore = Helpers.getInt(rootDict, "chaPoints");
|
||||||
|
Helpers.addSense(monster, rootDict, "blindsight");
|
||||||
|
// Helpers.getBool(rootDict, "blind");
|
||||||
|
Helpers.addSense(monster, rootDict, "darkvision");
|
||||||
|
Helpers.addSense(monster, rootDict, "tremorsense");
|
||||||
|
Helpers.addSense(monster, rootDict, "truesight");
|
||||||
|
monster.telepathyRange = Helpers.getInt(rootDict, "telepathy");
|
||||||
|
monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(Helpers.getString(rootDict, "cr"));
|
||||||
|
monster.customChallengeRatingDescription = Helpers.getString(rootDict, "customCr");
|
||||||
|
monster.customProficiencyBonus = Helpers.getInt(rootDict, "customProf");
|
||||||
|
// Helpers.getBool(rootDict, "isLegendary");
|
||||||
|
// Helpers.getString(rootDict, "legendariesDescription");
|
||||||
|
// Helpers.getBool(rootDict, "isLair");
|
||||||
|
// Helpers.getString(rootDict, "lairDescription");
|
||||||
|
// Helpers.getString(rootDict, "lairDescriptionEnd");
|
||||||
|
// Helpers.getBool(rootDict, "isRegional");
|
||||||
|
// Helpers.getString(rootDict, "regionalDescription");
|
||||||
|
// Helpers.getString(rootDict, "regionalDescriptionEnd");
|
||||||
|
// properties: []
|
||||||
|
monster.abilities = Helpers.getListOfTraits(rootDict, "abilities");
|
||||||
|
monster.actions = Helpers.getListOfTraits(rootDict, "actions");
|
||||||
|
monster.reactions = Helpers.getListOfTraits(rootDict, "reactions");
|
||||||
|
monster.legendaryActions = Helpers.getListOfTraits(rootDict, "legendaries");
|
||||||
|
monster.lairActions = Helpers.getListOfTraits(rootDict, "lairs");
|
||||||
|
monster.regionalActions = Helpers.getListOfTraits(rootDict, "regionals");
|
||||||
|
Helpers.addSavingThrows(monster, rootDict);
|
||||||
|
// skills: []
|
||||||
|
monster.skills = Helpers.getSetOfSkills(rootDict);
|
||||||
|
// damagetypes: []
|
||||||
|
// specialdamage: []
|
||||||
|
monster.damageImmunities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "i");
|
||||||
|
monster.damageImmunities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "i"));
|
||||||
|
monster.damageResistances = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "r");
|
||||||
|
monster.damageResistances.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "r"));
|
||||||
|
monster.damageVulnerabilities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "v");
|
||||||
|
monster.damageVulnerabilities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "v"));
|
||||||
|
// conditions: []
|
||||||
|
monster.conditionImmunities = Helpers.getSetOfDamageTypes(rootDict, "conditions");
|
||||||
|
// languages: []
|
||||||
|
monster.languages = Helpers.getSetOfLanguages(rootDict, "languages");
|
||||||
|
// understandsBut: ""
|
||||||
|
monster.understandsButDescription = Helpers.getString(rootDict, "understandsBut");
|
||||||
|
// shortName: ""
|
||||||
|
// doubleColumns: true
|
||||||
|
// separationPoint: -1
|
||||||
|
// damage: []
|
||||||
|
// pluralName: ""
|
||||||
|
|
||||||
|
return monster;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class Helpers {
|
||||||
|
public static String getString(JsonObject dict, String name) {
|
||||||
|
return getString(dict, name, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getString(@NonNull JsonObject dict, String name, String defaultValue) {
|
||||||
|
if (dict.has(name)) {
|
||||||
|
return dict.get(name).getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getInt(JsonObject dict, String name) {
|
||||||
|
return getInt(dict, name, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getInt(@NonNull JsonObject dict, String name, int defaultValue) {
|
||||||
|
if (dict.has(name)) {
|
||||||
|
JsonElement element = dict.get(name);
|
||||||
|
if (element.isJsonPrimitive()) {
|
||||||
|
JsonPrimitive rawValue = element.getAsJsonPrimitive();
|
||||||
|
if (rawValue.isNumber()) {
|
||||||
|
return rawValue.getAsInt();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return rawValue.getAsInt();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getBool(JsonObject dict, String name) {
|
||||||
|
return getBool(dict, name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) {
|
||||||
|
if (dict.has(name)) {
|
||||||
|
JsonElement element = dict.get(name);
|
||||||
|
if (element.isJsonPrimitive()) {
|
||||||
|
JsonPrimitive rawValue = element.getAsJsonPrimitive();
|
||||||
|
if (rawValue.isBoolean()) {
|
||||||
|
return rawValue.getAsBoolean();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return rawValue.getAsBoolean();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String formatDistance(String name, int distance) {
|
||||||
|
// TODO: consider moving this to a string resource so it can be localized
|
||||||
|
return String.format(Locale.getDefault(), "%s %d ft.", name, distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addSense(Monster monster, JsonObject root, String name) {
|
||||||
|
int distance = Helpers.getInt(root, name);
|
||||||
|
if (distance > 0) {
|
||||||
|
monster.senses.add(Helpers.formatDistance(name, distance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<Trait> getListOfTraits(@NonNull JsonObject dict, String name) {
|
||||||
|
ArrayList<Trait> traits = new ArrayList<>();
|
||||||
|
if (dict.has(name)) {
|
||||||
|
JsonElement arrayElement = dict.get(name);
|
||||||
|
if (arrayElement.isJsonArray()) {
|
||||||
|
JsonArray array = arrayElement.getAsJsonArray();
|
||||||
|
int size = array.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
JsonElement jsonElement = array.get(index);
|
||||||
|
if (jsonElement.isJsonObject()) {
|
||||||
|
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||||
|
String traitName = Helpers.getString(jsonObject, "name");
|
||||||
|
String description = Helpers.getString(jsonObject, "desc");
|
||||||
|
Trait trait = new Trait(traitName, description);
|
||||||
|
traits.add(trait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addSavingThrows(Monster monster, @NonNull JsonObject root) {
|
||||||
|
if (root.has("sthrows")) {
|
||||||
|
JsonElement arrayElement = root.get("sthrows");
|
||||||
|
if (arrayElement.isJsonArray()) {
|
||||||
|
JsonArray array = arrayElement.getAsJsonArray();
|
||||||
|
int size = array.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
JsonElement jsonElement = array.get(index);
|
||||||
|
if (jsonElement.isJsonObject()) {
|
||||||
|
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||||
|
String name = Helpers.getString(jsonObject, "name");
|
||||||
|
if ("str".equals(name)) {
|
||||||
|
monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
} else if ("dex".equals(name)) {
|
||||||
|
monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
} else if ("con".equals(name)) {
|
||||||
|
monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
} else if ("int".equals(name)) {
|
||||||
|
monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
} else if ("wis".equals(name)) {
|
||||||
|
monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
} else if ("cha".equals(name)) {
|
||||||
|
monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload.");
|
|
||||||
|
@NonNull
|
||||||
|
public static Set<Skill> getSetOfSkills(@NonNull JsonObject root) {
|
||||||
|
HashSet<Skill> skills = new HashSet<>();
|
||||||
|
if (root.has("skills")) {
|
||||||
|
JsonElement arrayElement = root.get("skills");
|
||||||
|
if (arrayElement.isJsonArray()) {
|
||||||
|
JsonArray array = arrayElement.getAsJsonArray();
|
||||||
|
int size = array.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
JsonElement jsonElement = array.get(index);
|
||||||
|
if (jsonElement.isJsonObject()) {
|
||||||
|
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||||
|
String name = Helpers.getString(jsonObject, "name");
|
||||||
|
String stat = Helpers.getString(jsonObject, "stat");
|
||||||
|
String note = Helpers.getString(jsonObject, "note");
|
||||||
|
|
||||||
|
Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT);
|
||||||
|
skills.add(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
|
||||||
|
return getSetOfDamageTypes(rootDict, name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Set<String> getSetOfDamageTypes(@NonNull JsonObject root, String name, String type) {
|
||||||
|
HashSet<String> damageTypes = new HashSet<>();
|
||||||
|
if (root.has(name)) {
|
||||||
|
JsonElement arrayElement = root.get(name);
|
||||||
|
if (arrayElement.isJsonArray()) {
|
||||||
|
JsonArray array = arrayElement.getAsJsonArray();
|
||||||
|
int size = array.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
JsonElement jsonElement = array.get(index);
|
||||||
|
if (jsonElement.isJsonObject()) {
|
||||||
|
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||||
|
String dtName = Helpers.getString(jsonObject, "name");
|
||||||
|
String dtType = Helpers.getString(jsonObject, "type");
|
||||||
|
if (type == null || type.equals(dtType)) {
|
||||||
|
damageTypes.add(dtName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return damageTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Set<Language> getSetOfLanguages(@NonNull JsonObject root, String name) {
|
||||||
|
HashSet<Language> languages = new HashSet<>();
|
||||||
|
if (root.has(name)) {
|
||||||
|
JsonElement arrayElement = root.get(name);
|
||||||
|
if (arrayElement.isJsonArray()) {
|
||||||
|
JsonArray array = arrayElement.getAsJsonArray();
|
||||||
|
int size = array.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
JsonElement jsonElement = array.get(index);
|
||||||
|
if (jsonElement.isJsonObject()) {
|
||||||
|
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||||
|
String languageName = Helpers.getString(jsonObject, "name");
|
||||||
|
boolean canSpeak = Helpers.getBool(jsonObject, "speaks");
|
||||||
|
Language language = new Language(languageName, canSpeak);
|
||||||
|
languages.add(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
|
||||||
import com.majinnaibu.monstercards.models.Language;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.models.Skill;
|
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
|
|
||||||
public class DnDBeyondImporter implements EntityImporter<Monster> {
|
|
||||||
|
|
||||||
private static final Pattern DNDBEYOND_URL_PATTERN = Pattern.compile("https?://(?:www\\.)?dndbeyond\\.com/characters/(\\d+)(?:/([a-zA-Z0-9]+))?", Pattern.CASE_INSENSITIVE);
|
|
||||||
private static final Pattern CHARACTER_ID_PATTERN = Pattern.compile("^\\d+$");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canImport(@NonNull String input) {
|
|
||||||
String trimmed = input.trim();
|
|
||||||
if (trimmed.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (DNDBEYOND_URL_PATTERN.matcher(trimmed).find() || CHARACTER_ID_PATTERN.matcher(trimmed).matches()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonElement element = JsonParser.parseString(trimmed);
|
|
||||||
if (element.isJsonObject()) {
|
|
||||||
JsonObject root = element.getAsJsonObject();
|
|
||||||
if (root.has("data") && root.getAsJsonObject("data").has("baseHitPoints")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Monster parse(@NonNull String input) throws Exception {
|
|
||||||
String trimmed = input.trim();
|
|
||||||
JsonObject characterData;
|
|
||||||
String sourceUrl = "";
|
|
||||||
|
|
||||||
if (trimmed.startsWith("{")) {
|
|
||||||
JsonObject root = JsonParser.parseString(trimmed).getAsJsonObject();
|
|
||||||
characterData = root.has("data") ? root.getAsJsonObject("data") : root;
|
|
||||||
} else {
|
|
||||||
String characterId = extractCharacterId(trimmed);
|
|
||||||
if (characterId == null) {
|
|
||||||
throw new IllegalArgumentException("Could not extract D&D Beyond character ID from input: " + input);
|
|
||||||
}
|
|
||||||
sourceUrl = trimmed.startsWith("http") ? trimmed : "https://www.dndbeyond.com/characters/" + characterId;
|
|
||||||
String jsonPayload = fetchCharacterJson(characterId);
|
|
||||||
JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();
|
|
||||||
if (!root.has("data") || root.get("data").isJsonNull()) {
|
|
||||||
throw new IllegalArgumentException("D&D Beyond response did not contain character data.");
|
|
||||||
}
|
|
||||||
characterData = root.getAsJsonObject("data");
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildMonsterFromCharacterData(characterData, sourceUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String extractCharacterId(@NonNull String input) {
|
|
||||||
String trimmed = input.trim();
|
|
||||||
Matcher urlMatcher = DNDBEYOND_URL_PATTERN.matcher(trimmed);
|
|
||||||
if (urlMatcher.find()) {
|
|
||||||
return urlMatcher.group(1);
|
|
||||||
}
|
|
||||||
if (CHARACTER_ID_PATTERN.matcher(trimmed).matches()) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private String fetchCharacterJson(@NonNull String characterId) throws Exception {
|
|
||||||
String serviceUrl = "https://character-service.dndbeyond.com/character/v5/character/" + characterId + "?includeCustomItems=true";
|
|
||||||
URL url = new URL(serviceUrl);
|
|
||||||
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Android; Mobile)");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
|
||||||
if (code != 200) {
|
|
||||||
throw new IllegalStateException("D&D Beyond service returned HTTP " + code);
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
try (InputStream in = conn.getInputStream();
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
sb.append(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Monster buildMonsterFromCharacterData(@NonNull JsonObject data, @NonNull String sourceUrl) {
|
|
||||||
Monster monster = new Monster();
|
|
||||||
monster.sourceUrl = sourceUrl;
|
|
||||||
|
|
||||||
String rawName = getString(data, "name");
|
|
||||||
if (rawName.startsWith("\"") && rawName.endsWith("\"") && rawName.length() > 1) {
|
|
||||||
rawName = rawName.substring(1, rawName.length() - 1);
|
|
||||||
}
|
|
||||||
monster.name = rawName.isEmpty() ? "D&D Beyond Character" : rawName;
|
|
||||||
|
|
||||||
monster.size = "Medium";
|
|
||||||
monster.type = "Humanoid";
|
|
||||||
|
|
||||||
parseClassesAndLevel(data, monster);
|
|
||||||
parseStats(data, monster);
|
|
||||||
parseHitPoints(data, monster);
|
|
||||||
parseSpeedAndSenses(data, monster);
|
|
||||||
parseModifiers(data, monster);
|
|
||||||
parseTraitsAndFeatures(data, monster);
|
|
||||||
parseActionsAndAttacks(data, monster);
|
|
||||||
|
|
||||||
return monster;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseClassesAndLevel(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
StringBuilder classSummary = new StringBuilder();
|
|
||||||
int totalLevel = 0;
|
|
||||||
if (data.has("classes") && data.get("classes").isJsonArray()) {
|
|
||||||
JsonArray classes = data.getAsJsonArray("classes");
|
|
||||||
for (int i = 0; i < classes.size(); i++) {
|
|
||||||
JsonObject cls = classes.get(i).getAsJsonObject();
|
|
||||||
int level = getInt(cls, "level", 1);
|
|
||||||
totalLevel += level;
|
|
||||||
String className = "";
|
|
||||||
if (cls.has("definition") && cls.get("definition").isJsonObject()) {
|
|
||||||
className = getString(cls.getAsJsonObject("definition"), "name");
|
|
||||||
}
|
|
||||||
String subclassName = "";
|
|
||||||
if (cls.has("subclassDefinition") && cls.get("subclassDefinition").isJsonObject()) {
|
|
||||||
subclassName = getString(cls.getAsJsonObject("subclassDefinition"), "name");
|
|
||||||
}
|
|
||||||
if (classSummary.length() > 0) {
|
|
||||||
classSummary.append(" / ");
|
|
||||||
}
|
|
||||||
classSummary.append(className);
|
|
||||||
if (!subclassName.isEmpty()) {
|
|
||||||
classSummary.append(" (").append(subclassName).append(")");
|
|
||||||
}
|
|
||||||
classSummary.append(" ").append(level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
monster.hitDice = totalLevel > 0 ? totalLevel : 1;
|
|
||||||
String raceName = "";
|
|
||||||
if (data.has("race") && data.get("race").isJsonObject()) {
|
|
||||||
JsonObject race = data.getAsJsonObject("race");
|
|
||||||
raceName = getString(race, "fullName");
|
|
||||||
if (raceName.isEmpty()) {
|
|
||||||
raceName = getString(race, "baseRaceName");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StringBuilder subtypeSb = new StringBuilder();
|
|
||||||
if (totalLevel > 0) {
|
|
||||||
subtypeSb.append("Level ").append(totalLevel);
|
|
||||||
}
|
|
||||||
if (!raceName.isEmpty()) {
|
|
||||||
if (subtypeSb.length() > 0) subtypeSb.append(" ");
|
|
||||||
subtypeSb.append(raceName);
|
|
||||||
}
|
|
||||||
if (classSummary.length() > 0) {
|
|
||||||
if (subtypeSb.length() > 0) subtypeSb.append(" ");
|
|
||||||
subtypeSb.append(classSummary);
|
|
||||||
}
|
|
||||||
monster.subtype = subtypeSb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStats(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
if (data.has("stats") && data.get("stats").isJsonArray()) {
|
|
||||||
JsonArray stats = data.getAsJsonArray("stats");
|
|
||||||
for (int i = 0; i < stats.size(); i++) {
|
|
||||||
JsonObject stat = stats.get(i).getAsJsonObject();
|
|
||||||
int id = getInt(stat, "id");
|
|
||||||
int val = getInt(stat, "value", 10);
|
|
||||||
switch (id) {
|
|
||||||
case 1: monster.strengthScore = val; break;
|
|
||||||
case 2: monster.dexterityScore = val; break;
|
|
||||||
case 3: monster.constitutionScore = val; break;
|
|
||||||
case 4: monster.intelligenceScore = val; break;
|
|
||||||
case 5: monster.wisdomScore = val; break;
|
|
||||||
case 6: monster.charismaScore = val; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseHitPoints(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
int baseHp = getInt(data, "baseHitPoints");
|
|
||||||
int overrideHp = getInt(data, "overrideHitPoints");
|
|
||||||
int maxHp = overrideHp > 0 ? overrideHp : baseHp;
|
|
||||||
if (maxHp > 0) {
|
|
||||||
monster.hasCustomHP = true;
|
|
||||||
monster.customHPDescription = String.format(Locale.ROOT, "%d (%dd%d)", maxHp, monster.hitDice, 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseSpeedAndSenses(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
if (data.has("race") && data.get("race").isJsonObject()) {
|
|
||||||
JsonObject race = data.getAsJsonObject("race");
|
|
||||||
if (race.has("weightSpeeds") && race.get("weightSpeeds").isJsonObject()) {
|
|
||||||
JsonObject weightSpeeds = race.getAsJsonObject("weightSpeeds");
|
|
||||||
if (weightSpeeds.has("normal") && weightSpeeds.get("normal").isJsonObject()) {
|
|
||||||
JsonObject normal = weightSpeeds.getAsJsonObject("normal");
|
|
||||||
monster.walkSpeed = getInt(normal, "walk", 30);
|
|
||||||
monster.flySpeed = getInt(normal, "fly", 0);
|
|
||||||
monster.burrowSpeed = getInt(normal, "burrow", 0);
|
|
||||||
monster.swimSpeed = getInt(normal, "swim", 0);
|
|
||||||
monster.climbSpeed = getInt(normal, "climb", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monster.walkSpeed == 0) {
|
|
||||||
monster.walkSpeed = 30;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseModifiers(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
if (!data.has("modifiers") || !data.get("modifiers").isJsonObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonObject modifiersObj = data.getAsJsonObject("modifiers");
|
|
||||||
String[] categories = new String[]{"race", "class", "background", "item", "feat"};
|
|
||||||
|
|
||||||
for (String cat : categories) {
|
|
||||||
if (modifiersObj.has(cat) && modifiersObj.get(cat).isJsonArray()) {
|
|
||||||
JsonArray mods = modifiersObj.getAsJsonArray(cat);
|
|
||||||
for (int i = 0; i < mods.size(); i++) {
|
|
||||||
JsonObject mod = mods.get(i).getAsJsonObject();
|
|
||||||
String type = getString(mod, "type");
|
|
||||||
String subType = getString(mod, "subType");
|
|
||||||
int fixedVal = getInt(mod, "fixedValue");
|
|
||||||
|
|
||||||
if ("set-base".equals(type) && "darkvision".equals(subType)) {
|
|
||||||
int dist = fixedVal > 0 ? fixedVal : 60;
|
|
||||||
monster.senses.add("darkvision " + dist + " ft.");
|
|
||||||
} else if ("language".equals(type)) {
|
|
||||||
String langName = capitalize(subType.replace("-", " "));
|
|
||||||
monster.languages.add(new Language(langName, true));
|
|
||||||
} else if ("proficiency".equals(type)) {
|
|
||||||
if (subType.endsWith("-saving-throws")) {
|
|
||||||
String stat = subType.replace("-saving-throws", "");
|
|
||||||
applySavingThrowProficiency(monster, stat);
|
|
||||||
} else {
|
|
||||||
applySkillProficiency(monster, subType, ProficiencyType.PROFICIENT);
|
|
||||||
}
|
|
||||||
} else if ("expertise".equals(type)) {
|
|
||||||
applySkillProficiency(monster, subType, ProficiencyType.EXPERTISE);
|
|
||||||
} else if ("immunity".equals(type)) {
|
|
||||||
monster.damageImmunities.add(capitalize(subType.replace("-", " ")));
|
|
||||||
} else if ("resistance".equals(type)) {
|
|
||||||
monster.damageResistances.add(capitalize(subType.replace("-", " ")));
|
|
||||||
} else if ("vulnerability".equals(type)) {
|
|
||||||
monster.damageVulnerabilities.add(capitalize(subType.replace("-", " ")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applySavingThrowProficiency(@NonNull Monster monster, String stat) {
|
|
||||||
if ("strength".equals(stat) || "str".equals(stat)) monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if ("dexterity".equals(stat) || "dex".equals(stat)) monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if ("constitution".equals(stat) || "con".equals(stat)) monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if ("intelligence".equals(stat) || "int".equals(stat)) monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if ("wisdom".equals(stat) || "wis".equals(stat)) monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if ("charisma".equals(stat) || "cha".equals(stat)) monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applySkillProficiency(@NonNull Monster monster, String subType, ProficiencyType profType) {
|
|
||||||
String skillName = capitalize(subType.replace("-", " "));
|
|
||||||
AbilityScore abilityScore = AbilityScore.STRENGTH;
|
|
||||||
if ("Acrobatics".equalsIgnoreCase(skillName) || "Sleight Of Hand".equalsIgnoreCase(skillName) || "Stealth".equalsIgnoreCase(skillName)) {
|
|
||||||
abilityScore = AbilityScore.DEXTERITY;
|
|
||||||
} else if ("Arcana".equalsIgnoreCase(skillName) || "History".equalsIgnoreCase(skillName) || "Investigation".equalsIgnoreCase(skillName) || "Nature".equalsIgnoreCase(skillName) || "Religion".equalsIgnoreCase(skillName)) {
|
|
||||||
abilityScore = AbilityScore.INTELLIGENCE;
|
|
||||||
} else if ("Animal Handling".equalsIgnoreCase(skillName) || "Insight".equalsIgnoreCase(skillName) || "Medicine".equalsIgnoreCase(skillName) || "Perception".equalsIgnoreCase(skillName) || "Survival".equalsIgnoreCase(skillName)) {
|
|
||||||
abilityScore = AbilityScore.WISDOM;
|
|
||||||
} else if ("Deception".equalsIgnoreCase(skillName) || "Intimidation".equalsIgnoreCase(skillName) || "Performance".equalsIgnoreCase(skillName) || "Persuasion".equalsIgnoreCase(skillName)) {
|
|
||||||
abilityScore = AbilityScore.CHARISMA;
|
|
||||||
} else if ("Athletics".equalsIgnoreCase(skillName)) {
|
|
||||||
abilityScore = AbilityScore.STRENGTH;
|
|
||||||
}
|
|
||||||
monster.skills.add(new Skill(skillName, abilityScore, AdvantageType.NONE, profType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseTraitsAndFeatures(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
if (data.has("race") && data.get("race").isJsonObject()) {
|
|
||||||
JsonObject race = data.getAsJsonObject("race");
|
|
||||||
if (race.has("racialTraits") && race.get("racialTraits").isJsonArray()) {
|
|
||||||
JsonArray traits = race.getAsJsonArray("racialTraits");
|
|
||||||
for (int i = 0; i < traits.size(); i++) {
|
|
||||||
JsonObject tr = traits.get(i).getAsJsonObject();
|
|
||||||
if (tr.has("definition") && tr.get("definition").isJsonObject()) {
|
|
||||||
JsonObject def = tr.getAsJsonObject("definition");
|
|
||||||
String name = getString(def, "name");
|
|
||||||
String desc = stripHtml(getString(def, "snippet"));
|
|
||||||
if (desc.isEmpty()) {
|
|
||||||
desc = stripHtml(getString(def, "description"));
|
|
||||||
}
|
|
||||||
if (!name.isEmpty() && !desc.isEmpty()) {
|
|
||||||
monster.abilities.add(new Trait(name, desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseActionsAndAttacks(@NonNull JsonObject data, @NonNull Monster monster) {
|
|
||||||
if (data.has("actions") && data.get("actions").isJsonObject()) {
|
|
||||||
JsonObject actionsObj = data.getAsJsonObject("actions");
|
|
||||||
String[] categories = new String[]{"class", "race", "background", "item", "feat"};
|
|
||||||
for (String cat : categories) {
|
|
||||||
if (actionsObj.has(cat) && actionsObj.get(cat).isJsonArray()) {
|
|
||||||
JsonArray acts = actionsObj.getAsJsonArray(cat);
|
|
||||||
for (int i = 0; i < acts.size(); i++) {
|
|
||||||
JsonObject act = acts.get(i).getAsJsonObject();
|
|
||||||
String name = getString(act, "name");
|
|
||||||
String desc = stripHtml(getString(act, "snippet"));
|
|
||||||
if (desc.isEmpty()) {
|
|
||||||
desc = stripHtml(getString(act, "description"));
|
|
||||||
}
|
|
||||||
int actType = 1;
|
|
||||||
if (act.has("activation") && act.get("activation").isJsonObject()) {
|
|
||||||
actType = getInt(act.getAsJsonObject("activation"), "activationType", 1);
|
|
||||||
}
|
|
||||||
if (!name.isEmpty()) {
|
|
||||||
Trait trait = new Trait(name, desc);
|
|
||||||
if (actType == 3) {
|
|
||||||
monster.bonusActions.add(trait);
|
|
||||||
} else if (actType == 4) {
|
|
||||||
monster.reactions.add(trait);
|
|
||||||
} else {
|
|
||||||
monster.actions.add(trait);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(@NonNull JsonObject obj, String key) {
|
|
||||||
return getString(obj, key, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(@NonNull JsonObject obj, String key, String defaultVal) {
|
|
||||||
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
|
||||||
return obj.get(key).getAsString();
|
|
||||||
}
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getInt(@NonNull JsonObject obj, String key) {
|
|
||||||
return getInt(obj, key, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getInt(@NonNull JsonObject obj, String key, int defaultVal) {
|
|
||||||
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
|
||||||
try {
|
|
||||||
return obj.get(key).getAsInt();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripHtml(String html) {
|
|
||||||
if (html == null) return "";
|
|
||||||
return html.replaceAll("<[^>]*>", "").replaceAll(" ", " ").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String capitalize(String str) {
|
|
||||||
if (str == null || str.isEmpty()) return str;
|
|
||||||
String[] words = str.split("\\s+");
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (String w : words) {
|
|
||||||
if (!w.isEmpty()) {
|
|
||||||
if (sb.length() > 0) sb.append(" ");
|
|
||||||
sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1).toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public interface EntityImporter<T> {
|
|
||||||
/**
|
|
||||||
* Determines whether the given input string or payload can be handled by this importer.
|
|
||||||
*/
|
|
||||||
boolean canImport(@NonNull String input);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the raw input string into internal domain memory objects (e.g., Monster).
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
T parse(@NonNull String input) throws Exception;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
public class MonsterJsonImporter implements EntityImporter<Monster> {
|
|
||||||
|
|
||||||
private static final Gson GSON = new Gson();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canImport(@NonNull String input) {
|
|
||||||
try {
|
|
||||||
JsonElement el = JsonParser.parseString(input);
|
|
||||||
if (el.isJsonObject()) {
|
|
||||||
JsonObject obj = el.getAsJsonObject();
|
|
||||||
return obj.has("name") && (obj.has("strengthScore") || obj.has("hitDice") || obj.has("size"));
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Monster parse(@NonNull String input) throws Exception {
|
|
||||||
Monster monster = GSON.fromJson(input, Monster.class);
|
|
||||||
if (monster == null) {
|
|
||||||
throw new IllegalArgumentException("Failed to deserialize Monster JSON");
|
|
||||||
}
|
|
||||||
return monster;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ArmorType;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
|
||||||
import com.majinnaibu.monstercards.models.Language;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.models.Skill;
|
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class Open5eImporter implements EntityImporter<Monster> {
|
|
||||||
|
|
||||||
private static final Pattern HIT_DICE_PATTERN = Pattern.compile("(\\d+)d(\\d+)");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canImport(@NonNull String input) {
|
|
||||||
String trimmed = input.trim();
|
|
||||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonElement element = JsonParser.parseString(trimmed);
|
|
||||||
if (!element.isJsonObject()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
JsonObject root = element.getAsJsonObject();
|
|
||||||
if (root.has("ruleset_id") && root.get("ruleset_id").getAsString().equals("open5e")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (root.has("properties") && root.getAsJsonObject("properties").has("abilities")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Monster parse(@NonNull String input) throws Exception {
|
|
||||||
JsonObject root = JsonParser.parseString(input.trim()).getAsJsonObject();
|
|
||||||
|
|
||||||
Monster monster = new Monster();
|
|
||||||
|
|
||||||
if (root.has("uuid") && !root.get("uuid").isJsonNull()) {
|
|
||||||
try {
|
|
||||||
monster.id = UUID.fromString(root.get("uuid").getAsString());
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.has("display_name") && !root.get("display_name").isJsonNull()) {
|
|
||||||
monster.name = root.get("display_name").getAsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject props = root.has("properties") ? root.getAsJsonObject("properties") : root;
|
|
||||||
|
|
||||||
if (monster.name == null || monster.name.isEmpty()) {
|
|
||||||
monster.name = getString(props, "name", "Unnamed Entity");
|
|
||||||
}
|
|
||||||
|
|
||||||
monster.size = getString(props, "size");
|
|
||||||
monster.type = getString(props, "type");
|
|
||||||
monster.subtype = getString(props, "subtype");
|
|
||||||
monster.alignment = getString(props, "alignment");
|
|
||||||
|
|
||||||
parseArmorClass(props, monster);
|
|
||||||
parseHitPoints(props, monster);
|
|
||||||
|
|
||||||
monster.walkSpeed = getInt(props, "speed", 30);
|
|
||||||
if (props.has("speed_desc")) {
|
|
||||||
String speedDesc = getString(props, "speed_desc");
|
|
||||||
if (!speedDesc.isEmpty()) {
|
|
||||||
monster.hasCustomSpeed = true;
|
|
||||||
monster.customSpeedDescription = speedDesc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseChallengeRating(props, monster);
|
|
||||||
parseAbilities(props, monster);
|
|
||||||
parseIdentityFields(props, monster);
|
|
||||||
|
|
||||||
parseSavingThrows(props, monster);
|
|
||||||
parseSkills(props, monster);
|
|
||||||
parseSenses(props, monster);
|
|
||||||
parseLanguages(props, monster);
|
|
||||||
parseDamageAndConditions(props, monster);
|
|
||||||
|
|
||||||
parseTraitsAndActions(props, monster);
|
|
||||||
|
|
||||||
if (props.has("source_url")) {
|
|
||||||
monster.sourceUrl = getString(props, "source_url");
|
|
||||||
}
|
|
||||||
|
|
||||||
return monster;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseArmorClass(JsonObject props, Monster monster) {
|
|
||||||
if (props.has("equipped_armor") && props.get("equipped_armor").isJsonObject()) {
|
|
||||||
JsonObject armorObj = props.getAsJsonObject("equipped_armor");
|
|
||||||
String armorId = getString(armorObj, "id");
|
|
||||||
ArmorType armorType = ArmorType.valueOfString(armorId);
|
|
||||||
if (armorType != ArmorType.NONE) {
|
|
||||||
monster.armorType = armorType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.has("equipped_shield") && props.get("equipped_shield").isJsonObject()) {
|
|
||||||
JsonObject shieldObj = props.getAsJsonObject("equipped_shield");
|
|
||||||
monster.shieldBonus = getInt(shieldObj, "ac_bonus", 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monster.armorType == ArmorType.NONE && props.has("armor_description")) {
|
|
||||||
String armorDesc = getString(props, "armor_description");
|
|
||||||
if (!armorDesc.isEmpty()) {
|
|
||||||
monster.otherArmorDescription = armorDesc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseHitPoints(JsonObject props, Monster monster) {
|
|
||||||
if (props.has("hit_points") && props.get("hit_points").isJsonObject()) {
|
|
||||||
JsonObject hpObj = props.getAsJsonObject("hit_points");
|
|
||||||
int maxHp = getInt(hpObj, "max", getInt(hpObj, "current", 10));
|
|
||||||
if (hpObj.has("hit_dice") && !hpObj.get("hit_dice").isJsonNull()) {
|
|
||||||
String hitDiceStr = hpObj.get("hit_dice").getAsString();
|
|
||||||
Matcher m = HIT_DICE_PATTERN.matcher(hitDiceStr);
|
|
||||||
if (m.find()) {
|
|
||||||
monster.hitDice = Integer.parseInt(m.group(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hpObj.has("formula") && !hpObj.get("formula").isJsonNull()) {
|
|
||||||
monster.hasCustomHP = true;
|
|
||||||
monster.customHPDescription = String.format(Locale.ROOT, "%d (%s)", maxHp, hpObj.get("formula").getAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseChallengeRating(JsonObject props, Monster monster) {
|
|
||||||
String crStr = "";
|
|
||||||
if (props.has("cr") && !props.get("cr").isJsonNull()) {
|
|
||||||
crStr = props.get("cr").getAsString();
|
|
||||||
} else if (props.has("challenge_rating") && !props.get("challenge_rating").isJsonNull()) {
|
|
||||||
crStr = props.get("challenge_rating").getAsString();
|
|
||||||
}
|
|
||||||
if (!crStr.isEmpty()) {
|
|
||||||
monster.challengeRating = ChallengeRating.valueOfString(crStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseAbilities(JsonObject props, Monster monster) {
|
|
||||||
if (!props.has("abilities") || !props.get("abilities").isJsonObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonObject abs = props.getAsJsonObject("abilities");
|
|
||||||
monster.strengthScore = extractAbilityScore(abs, "strength");
|
|
||||||
monster.dexterityScore = extractAbilityScore(abs, "dexterity");
|
|
||||||
monster.constitutionScore = extractAbilityScore(abs, "constitution");
|
|
||||||
monster.intelligenceScore = extractAbilityScore(abs, "intelligence");
|
|
||||||
monster.wisdomScore = extractAbilityScore(abs, "wisdom");
|
|
||||||
monster.charismaScore = extractAbilityScore(abs, "charisma");
|
|
||||||
}
|
|
||||||
|
|
||||||
private int extractAbilityScore(JsonObject abs, String key) {
|
|
||||||
if (!abs.has(key) || abs.get(key).isJsonNull()) {
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
JsonElement el = abs.get(key);
|
|
||||||
if (el.isJsonPrimitive()) {
|
|
||||||
return el.getAsInt();
|
|
||||||
} else if (el.isJsonObject() && el.getAsJsonObject().has("value")) {
|
|
||||||
return el.getAsJsonObject().get("value").getAsInt();
|
|
||||||
}
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseIdentityFields(JsonObject props, Monster monster) {
|
|
||||||
monster.playerName = getString(props, "player_name");
|
|
||||||
monster.background = getString(props, "background");
|
|
||||||
monster.personalityTraits = getString(props, "personality_traits");
|
|
||||||
monster.ideals = getString(props, "ideals");
|
|
||||||
monster.bonds = getString(props, "bonds");
|
|
||||||
monster.flaws = getString(props, "flaws");
|
|
||||||
|
|
||||||
if (props.has("character_details") && props.get("character_details").isJsonObject()) {
|
|
||||||
JsonObject details = props.getAsJsonObject("character_details");
|
|
||||||
monster.age = getString(details, "age");
|
|
||||||
monster.height = getString(details, "height");
|
|
||||||
monster.weight = getString(details, "weight");
|
|
||||||
monster.eyes = getString(details, "eyes");
|
|
||||||
monster.skin = getString(details, "skin");
|
|
||||||
monster.hair = getString(details, "hair");
|
|
||||||
monster.appearance = getString(details, "appearance");
|
|
||||||
monster.backstory = getString(details, "backstory");
|
|
||||||
monster.alliesAndOrganizations = getString(details, "allies_and_organizations");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseSavingThrows(JsonObject props, Monster monster) {
|
|
||||||
if (!props.has("saving_throws")) return;
|
|
||||||
JsonElement savesEl = props.get("saving_throws");
|
|
||||||
if (savesEl.isJsonArray()) {
|
|
||||||
JsonArray arr = savesEl.getAsJsonArray();
|
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
|
||||||
String saveStr = arr.get(i).getAsString().toLowerCase(Locale.ROOT);
|
|
||||||
applySaveProficiency(monster, saveStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applySaveProficiency(Monster monster, String saveStr) {
|
|
||||||
if (saveStr.contains("str")) monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if (saveStr.contains("dex")) monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if (saveStr.contains("con")) monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if (saveStr.contains("int")) monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if (saveStr.contains("wis")) monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
else if (saveStr.contains("cha")) monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseSkills(JsonObject props, Monster monster) {
|
|
||||||
if (!props.has("skills")) return;
|
|
||||||
JsonElement skillsEl = props.get("skills");
|
|
||||||
if (skillsEl.isJsonArray()) {
|
|
||||||
JsonArray arr = skillsEl.getAsJsonArray();
|
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
|
||||||
String skillStr = arr.get(i).getAsString();
|
|
||||||
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
|
|
||||||
if (!skillName.isEmpty()) {
|
|
||||||
AbilityScore stat = getAbilityForSkill(skillName);
|
|
||||||
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AbilityScore getAbilityForSkill(String skillName) {
|
|
||||||
String lower = skillName.toLowerCase(Locale.ROOT);
|
|
||||||
if (lower.contains("acro") || lower.contains("sleight") || lower.contains("stealth")) return AbilityScore.DEXTERITY;
|
|
||||||
if (lower.contains("arca") || lower.contains("hist") || lower.contains("inve") || lower.contains("natu") || lower.contains("reli")) return AbilityScore.INTELLIGENCE;
|
|
||||||
if (lower.contains("anim") || lower.contains("insi") || lower.contains("medi") || lower.contains("perc") || lower.contains("surv")) return AbilityScore.WISDOM;
|
|
||||||
if (lower.contains("dece") || lower.contains("inti") || lower.contains("perf") || lower.contains("pers")) return AbilityScore.CHARISMA;
|
|
||||||
return AbilityScore.STRENGTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseSenses(JsonObject props, Monster monster) {
|
|
||||||
if (props.has("senses")) {
|
|
||||||
String sensesStr = getString(props, "senses");
|
|
||||||
if (!sensesStr.isEmpty()) {
|
|
||||||
String[] parts = sensesStr.split(",");
|
|
||||||
for (String p : parts) {
|
|
||||||
if (!p.trim().isEmpty()) {
|
|
||||||
monster.senses.add(p.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseLanguages(JsonObject props, Monster monster) {
|
|
||||||
if (props.has("languages")) {
|
|
||||||
JsonElement langEl = props.get("languages");
|
|
||||||
if (langEl.isJsonArray()) {
|
|
||||||
JsonArray arr = langEl.getAsJsonArray();
|
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
|
||||||
String langName = arr.get(i).getAsString();
|
|
||||||
if (!langName.isEmpty()) {
|
|
||||||
monster.languages.add(new Language(langName, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (langEl.isJsonPrimitive()) {
|
|
||||||
String langStr = langEl.getAsString();
|
|
||||||
for (String l : langStr.split(",")) {
|
|
||||||
if (!l.trim().isEmpty()) {
|
|
||||||
monster.languages.add(new Language(l.trim(), true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseDamageAndConditions(JsonObject props, Monster monster) {
|
|
||||||
parseStringSet(props, "damage_immunities", monster.damageImmunities);
|
|
||||||
parseStringSet(props, "damage_resistances", monster.damageResistances);
|
|
||||||
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
|
|
||||||
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
|
|
||||||
if (!props.has(key)) return;
|
|
||||||
JsonElement el = props.get(key);
|
|
||||||
if (el.isJsonArray()) {
|
|
||||||
JsonArray arr = el.getAsJsonArray();
|
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
|
||||||
String val = arr.get(i).getAsString();
|
|
||||||
if (!val.isEmpty()) set.add(capitalize(val));
|
|
||||||
}
|
|
||||||
} else if (el.isJsonPrimitive()) {
|
|
||||||
for (String val : el.getAsString().split(",")) {
|
|
||||||
if (!val.trim().isEmpty()) set.add(capitalize(val.trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseTraitsAndActions(JsonObject props, Monster monster) {
|
|
||||||
parseTraitList(props, "traits", monster.abilities);
|
|
||||||
parseTraitList(props, "actions", monster.actions);
|
|
||||||
parseTraitList(props, "bonus_actions", monster.bonusActions);
|
|
||||||
parseTraitList(props, "reactions", monster.reactions);
|
|
||||||
parseTraitList(props, "legendary_actions", monster.legendaryActions);
|
|
||||||
parseTraitList(props, "lair_actions", monster.lairActions);
|
|
||||||
parseTraitList(props, "regional_effects", monster.regionalActions);
|
|
||||||
parseTraitList(props, "mythic_actions", monster.mythicActions);
|
|
||||||
|
|
||||||
if (props.has("legendary_desc")) monster.legendaryActionsDescription = getString(props, "legendary_desc");
|
|
||||||
if (props.has("lair_desc")) monster.lairActionsDescription = getString(props, "lair_desc");
|
|
||||||
if (props.has("lair_end_note")) monster.lairActionsEndNote = getString(props, "lair_end_note");
|
|
||||||
if (props.has("regional_desc")) monster.regionalActionsDescription = getString(props, "regional_desc");
|
|
||||||
if (props.has("regional_end_note")) monster.regionalActionsEndNote = getString(props, "regional_end_note");
|
|
||||||
if (props.has("mythic_desc")) monster.mythicActionsDescription = getString(props, "mythic_desc");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseTraitList(JsonObject props, String key, List<Trait> list) {
|
|
||||||
if (!props.has(key) || !props.get(key).isJsonArray()) return;
|
|
||||||
JsonArray arr = props.getAsJsonArray(key);
|
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
|
||||||
JsonElement el = arr.get(i);
|
|
||||||
if (el.isJsonObject()) {
|
|
||||||
JsonObject obj = el.getAsJsonObject();
|
|
||||||
String name = getString(obj, "name");
|
|
||||||
String desc = getString(obj, "desc");
|
|
||||||
if (desc.isEmpty()) desc = getString(obj, "description");
|
|
||||||
if (!name.isEmpty()) {
|
|
||||||
list.add(new Trait(name, desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(JsonObject obj, String key) {
|
|
||||||
return getString(obj, key, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(JsonObject obj, String key, String defaultVal) {
|
|
||||||
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
|
||||||
return obj.get(key).getAsString();
|
|
||||||
}
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getInt(JsonObject obj, String key, int defaultVal) {
|
|
||||||
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
|
||||||
try {
|
|
||||||
return obj.get(key).getAsInt();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String capitalize(String str) {
|
|
||||||
if (str == null || str.isEmpty()) return str;
|
|
||||||
String[] words = str.split("\\s+");
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (String w : words) {
|
|
||||||
if (!w.isEmpty()) {
|
|
||||||
if (sb.length() > 0) sb.append(" ");
|
|
||||||
sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1).toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.google.gson.JsonPrimitive;
|
|
||||||
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
|
|
||||||
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
|
||||||
import com.majinnaibu.monstercards.models.Language;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.models.Skill;
|
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canImport(@NonNull String input) {
|
|
||||||
if (input.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonElement element = JsonParser.parseString(input);
|
|
||||||
if (!element.isJsonObject()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
JsonObject rootDict = element.getAsJsonObject();
|
|
||||||
return rootDict.has("armorName") || rootDict.has("strPoints")
|
|
||||||
|| rootDict.has("isLegendary") || rootDict.has("isMythic")
|
|
||||||
|| rootDict.has("legendariesDescription");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Monster parse(@NonNull String input) throws Exception {
|
|
||||||
JsonObject rootDict = JsonParser.parseString(input).getAsJsonObject();
|
|
||||||
|
|
||||||
Monster monster = new Monster();
|
|
||||||
monster.name = getString(rootDict, "name");
|
|
||||||
monster.size = getString(rootDict, "size");
|
|
||||||
monster.type = getString(rootDict, "type");
|
|
||||||
monster.subtype = getString(rootDict, "tag");
|
|
||||||
monster.alignment = getString(rootDict, "alignment");
|
|
||||||
monster.hitDice = getInt(rootDict, "hitDice");
|
|
||||||
monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(getString(rootDict, "armorName"));
|
|
||||||
monster.shieldBonus = getInt(rootDict, "shieldBonus");
|
|
||||||
monster.naturalArmorBonus = getInt(rootDict, "natArmorBonus");
|
|
||||||
monster.otherArmorDescription = getString(rootDict, "otherArmorDesc");
|
|
||||||
monster.walkSpeed = getInt(rootDict, "speed");
|
|
||||||
monster.burrowSpeed = getInt(rootDict, "burrowSpeed");
|
|
||||||
monster.climbSpeed = getInt(rootDict, "climbSpeed");
|
|
||||||
monster.flySpeed = getInt(rootDict, "flySpeed");
|
|
||||||
monster.canHover = getBool(rootDict, "hover");
|
|
||||||
monster.swimSpeed = getInt(rootDict, "swimSpeed");
|
|
||||||
monster.hasCustomHP = getBool(rootDict, "customHP");
|
|
||||||
monster.hasCustomSpeed = getBool(rootDict, "customSpeed");
|
|
||||||
monster.customHPDescription = getString(rootDict, "hpText");
|
|
||||||
monster.customSpeedDescription = getString(rootDict, "speedDesc");
|
|
||||||
monster.strengthScore = getInt(rootDict, "strPoints");
|
|
||||||
monster.dexterityScore = getInt(rootDict, "dexPoints");
|
|
||||||
monster.constitutionScore = getInt(rootDict, "conPoints");
|
|
||||||
monster.intelligenceScore = getInt(rootDict, "intPoints");
|
|
||||||
monster.wisdomScore = getInt(rootDict, "wisPoints");
|
|
||||||
monster.charismaScore = getInt(rootDict, "chaPoints");
|
|
||||||
addSense(monster, rootDict, "blindsight");
|
|
||||||
addSense(monster, rootDict, "darkvision");
|
|
||||||
addSense(monster, rootDict, "tremorsense");
|
|
||||||
addSense(monster, rootDict, "truesight");
|
|
||||||
monster.telepathyRange = getInt(rootDict, "telepathy");
|
|
||||||
monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(getString(rootDict, "cr"));
|
|
||||||
monster.customChallengeRatingDescription = getString(rootDict, "customCr");
|
|
||||||
monster.customProficiencyBonus = getInt(rootDict, "customProf");
|
|
||||||
|
|
||||||
monster.abilities = getListOfTraits(rootDict, "abilities");
|
|
||||||
monster.actions = getListOfTraits(rootDict, "actions");
|
|
||||||
monster.bonusActions = getListOfTraits(rootDict, "bonusActions");
|
|
||||||
monster.reactions = getListOfTraits(rootDict, "reactions");
|
|
||||||
monster.legendaryActions = getListOfTraits(rootDict, "legendaries");
|
|
||||||
monster.mythicActions = getListOfTraits(rootDict, "mythics");
|
|
||||||
monster.lairActions = getListOfTraits(rootDict, "lairs");
|
|
||||||
monster.regionalActions = getListOfTraits(rootDict, "regionals");
|
|
||||||
|
|
||||||
monster.legendaryActionsDescription = getString(rootDict, "legendariesDescription");
|
|
||||||
if (!monster.legendaryActionsDescription.isEmpty()) {
|
|
||||||
monster.legendaryActions.add(0, new Trait("Legendary Actions", monster.legendaryActionsDescription));
|
|
||||||
}
|
|
||||||
|
|
||||||
monster.mythicActionsDescription = getString(rootDict, "mythicDescription");
|
|
||||||
if (!monster.mythicActionsDescription.isEmpty() || !monster.mythicActions.isEmpty()) {
|
|
||||||
if (!monster.mythicActionsDescription.isEmpty()) {
|
|
||||||
monster.legendaryActions.add(new Trait("Mythic Actions", monster.mythicActionsDescription));
|
|
||||||
}
|
|
||||||
for (Trait mythic : monster.mythicActions) {
|
|
||||||
String traitName = mythic.name;
|
|
||||||
if (!traitName.toLowerCase(Locale.ROOT).startsWith("mythic action")) {
|
|
||||||
traitName = "Mythic Action: " + traitName;
|
|
||||||
}
|
|
||||||
monster.legendaryActions.add(new Trait(traitName, mythic.description));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
monster.lairActionsDescription = getString(rootDict, "lairDescription");
|
|
||||||
monster.lairActionsEndNote = getString(rootDict, "lairDescriptionEnd");
|
|
||||||
String fullLairDesc = combineDescriptions(monster.lairActionsDescription, monster.lairActionsEndNote);
|
|
||||||
if (!fullLairDesc.isEmpty()) {
|
|
||||||
monster.lairActions.add(0, new Trait("Lair Actions", fullLairDesc));
|
|
||||||
}
|
|
||||||
|
|
||||||
monster.regionalActionsDescription = getString(rootDict, "regionalDescription");
|
|
||||||
monster.regionalActionsEndNote = getString(rootDict, "regionalDescriptionEnd");
|
|
||||||
String fullRegionalDesc = combineDescriptions(monster.regionalActionsDescription, monster.regionalActionsEndNote);
|
|
||||||
if (!fullRegionalDesc.isEmpty()) {
|
|
||||||
monster.regionalActions.add(0, new Trait("Regional Effects", fullRegionalDesc));
|
|
||||||
}
|
|
||||||
|
|
||||||
addSavingThrows(monster, rootDict);
|
|
||||||
monster.skills = getSetOfSkills(rootDict);
|
|
||||||
monster.damageImmunities = getSetOfDamageTypes(rootDict, "damageTypes", "i");
|
|
||||||
monster.damageImmunities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "i"));
|
|
||||||
monster.damageResistances = getSetOfDamageTypes(rootDict, "damageTypes", "r");
|
|
||||||
monster.damageResistances.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "r"));
|
|
||||||
monster.damageVulnerabilities = getSetOfDamageTypes(rootDict, "damageTypes", "v");
|
|
||||||
monster.damageVulnerabilities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "v"));
|
|
||||||
monster.conditionImmunities = getSetOfDamageTypes(rootDict, "conditions");
|
|
||||||
monster.languages = getSetOfLanguages(rootDict, "languages");
|
|
||||||
monster.understandsButDescription = getString(rootDict, "understandsBut");
|
|
||||||
|
|
||||||
return monster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static String combineDescriptions(String desc1, String desc2) {
|
|
||||||
if (!desc1.isEmpty() && !desc2.isEmpty()) {
|
|
||||||
return desc1 + "\n\n" + desc2;
|
|
||||||
} else if (!desc1.isEmpty()) {
|
|
||||||
return desc1;
|
|
||||||
} else {
|
|
||||||
return desc2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(JsonObject dict, String name) {
|
|
||||||
return getString(dict, name, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(@NonNull JsonObject dict, String name, String defaultValue) {
|
|
||||||
if (dict.has(name)) {
|
|
||||||
return dict.get(name).getAsString();
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getInt(JsonObject dict, String name) {
|
|
||||||
return getInt(dict, name, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getInt(@NonNull JsonObject dict, String name, int defaultValue) {
|
|
||||||
if (dict.has(name)) {
|
|
||||||
JsonElement element = dict.get(name);
|
|
||||||
if (element.isJsonPrimitive()) {
|
|
||||||
JsonPrimitive rawValue = element.getAsJsonPrimitive();
|
|
||||||
if (rawValue.isNumber()) {
|
|
||||||
return rawValue.getAsInt();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return rawValue.getAsInt();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean getBool(JsonObject dict, String name) {
|
|
||||||
return getBool(dict, name, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) {
|
|
||||||
if (dict.has(name)) {
|
|
||||||
JsonElement element = dict.get(name);
|
|
||||||
if (element.isJsonPrimitive()) {
|
|
||||||
JsonPrimitive rawValue = element.getAsJsonPrimitive();
|
|
||||||
if (rawValue.isBoolean()) {
|
|
||||||
return rawValue.getAsBoolean();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return rawValue.getAsBoolean();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static String formatDistance(String name, int distance) {
|
|
||||||
return String.format(Locale.getDefault(), "%s %d ft.", name, distance);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addSense(Monster monster, JsonObject root, String name) {
|
|
||||||
int distance = getInt(root, name);
|
|
||||||
if (distance > 0) {
|
|
||||||
String senseStr = formatDistance(name, distance);
|
|
||||||
if ("blindsight".equals(name) && getBool(root, "blind")) {
|
|
||||||
senseStr += " (blind beyond this radius)";
|
|
||||||
}
|
|
||||||
monster.senses.add(senseStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static List<Trait> getListOfTraits(@NonNull JsonObject dict, String name) {
|
|
||||||
ArrayList<Trait> traits = new ArrayList<>();
|
|
||||||
if (dict.has(name)) {
|
|
||||||
JsonElement arrayElement = dict.get(name);
|
|
||||||
if (arrayElement.isJsonArray()) {
|
|
||||||
JsonArray array = arrayElement.getAsJsonArray();
|
|
||||||
int size = array.size();
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
JsonElement jsonElement = array.get(index);
|
|
||||||
if (jsonElement.isJsonObject()) {
|
|
||||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
|
||||||
String traitName = getString(jsonObject, "name");
|
|
||||||
String description = getString(jsonObject, "desc");
|
|
||||||
Trait trait = new Trait(traitName, description);
|
|
||||||
traits.add(trait);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return traits;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addSavingThrows(Monster monster, @NonNull JsonObject root) {
|
|
||||||
if (root.has("sthrows")) {
|
|
||||||
JsonElement arrayElement = root.get("sthrows");
|
|
||||||
if (arrayElement.isJsonArray()) {
|
|
||||||
JsonArray array = arrayElement.getAsJsonArray();
|
|
||||||
int size = array.size();
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
JsonElement jsonElement = array.get(index);
|
|
||||||
if (jsonElement.isJsonObject()) {
|
|
||||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
|
||||||
String name = getString(jsonObject, "name");
|
|
||||||
if ("str".equals(name)) {
|
|
||||||
monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
} else if ("dex".equals(name)) {
|
|
||||||
monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
} else if ("con".equals(name)) {
|
|
||||||
monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
} else if ("int".equals(name)) {
|
|
||||||
monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
} else if ("wis".equals(name)) {
|
|
||||||
monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
} else if ("cha".equals(name)) {
|
|
||||||
monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static Set<Skill> getSetOfSkills(@NonNull JsonObject root) {
|
|
||||||
HashSet<Skill> skills = new HashSet<>();
|
|
||||||
if (root.has("skills")) {
|
|
||||||
JsonElement arrayElement = root.get("skills");
|
|
||||||
if (arrayElement.isJsonArray()) {
|
|
||||||
JsonArray array = arrayElement.getAsJsonArray();
|
|
||||||
int size = array.size();
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
JsonElement jsonElement = array.get(index);
|
|
||||||
if (jsonElement.isJsonObject()) {
|
|
||||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
|
||||||
String name = getString(jsonObject, "name");
|
|
||||||
String stat = getString(jsonObject, "stat");
|
|
||||||
String note = getString(jsonObject, "note");
|
|
||||||
|
|
||||||
Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT);
|
|
||||||
skills.add(skill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return skills;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
|
|
||||||
return getSetOfDamageTypes(rootDict, name, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name, String type) {
|
|
||||||
HashSet<String> damageTypes = new HashSet<>();
|
|
||||||
if (rootDict.has(name)) {
|
|
||||||
JsonElement arrayElement = rootDict.get(name);
|
|
||||||
if (arrayElement.isJsonArray()) {
|
|
||||||
JsonArray array = arrayElement.getAsJsonArray();
|
|
||||||
int size = array.size();
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
JsonElement jsonElement = array.get(index);
|
|
||||||
if (jsonElement.isJsonObject()) {
|
|
||||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
|
||||||
String dtName = getString(jsonObject, "name");
|
|
||||||
String dtType = getString(jsonObject, "type");
|
|
||||||
if (type == null || type.equals(dtType)) {
|
|
||||||
damageTypes.add(dtName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return damageTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static Set<Language> getSetOfLanguages(@NonNull JsonObject root, String name) {
|
|
||||||
HashSet<Language> languages = new HashSet<>();
|
|
||||||
if (root.has(name)) {
|
|
||||||
JsonElement arrayElement = root.get(name);
|
|
||||||
if (arrayElement.isJsonArray()) {
|
|
||||||
JsonArray array = arrayElement.getAsJsonArray();
|
|
||||||
int size = array.size();
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
JsonElement jsonElement = array.get(index);
|
|
||||||
if (jsonElement.isJsonObject()) {
|
|
||||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
|
||||||
String languageName = getString(jsonObject, "name");
|
|
||||||
boolean canSpeak = getBool(jsonObject, "speaks");
|
|
||||||
Language language = new Language(languageName, canSpeak);
|
|
||||||
languages.add(language);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return languages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.room.Entity;
|
|
||||||
import androidx.room.PrimaryKey;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity(tableName = "collections")
|
|
||||||
public class Collection {
|
|
||||||
|
|
||||||
@PrimaryKey
|
|
||||||
@NonNull
|
|
||||||
public UUID id;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(defaultValue = "")
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(defaultValue = "")
|
|
||||||
public String description;
|
|
||||||
|
|
||||||
public Collection() {
|
|
||||||
this.id = UUID.randomUUID();
|
|
||||||
this.name = "";
|
|
||||||
this.description = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection(@NonNull String name, @NonNull String description) {
|
|
||||||
this.id = UUID.randomUUID();
|
|
||||||
this.name = name;
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object obj) {
|
|
||||||
if (this == obj) return true;
|
|
||||||
if (obj == null || getClass() != obj.getClass()) return false;
|
|
||||||
Collection that = (Collection) obj;
|
|
||||||
return Objects.equals(id, that.id) &&
|
|
||||||
Objects.equals(name, that.name) &&
|
|
||||||
Objects.equals(description, that.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(id, name, description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.room.Entity;
|
|
||||||
import androidx.room.ForeignKey;
|
|
||||||
import androidx.room.Index;
|
|
||||||
import androidx.room.PrimaryKey;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity(
|
|
||||||
tableName = "collection_monsters",
|
|
||||||
foreignKeys = {
|
|
||||||
@ForeignKey(
|
|
||||||
entity = Collection.class,
|
|
||||||
parentColumns = "id",
|
|
||||||
childColumns = "collection_id",
|
|
||||||
onDelete = ForeignKey.CASCADE
|
|
||||||
),
|
|
||||||
@ForeignKey(
|
|
||||||
entity = Monster.class,
|
|
||||||
parentColumns = "id",
|
|
||||||
childColumns = "monster_id",
|
|
||||||
onDelete = ForeignKey.CASCADE
|
|
||||||
)
|
|
||||||
},
|
|
||||||
indices = {
|
|
||||||
@Index(value = {"collection_id"}),
|
|
||||||
@Index(value = {"monster_id"})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public class CollectionMonster {
|
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
public long id;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "collection_id")
|
|
||||||
public UUID collectionId;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "monster_id")
|
|
||||||
public UUID monsterId;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "ordinal", defaultValue = "0")
|
|
||||||
public int ordinal;
|
|
||||||
|
|
||||||
public CollectionMonster() {
|
|
||||||
this.collectionId = UUID.fromString("00000000-0000-0000-0000-000000000000");
|
|
||||||
this.monsterId = UUID.fromString("00000000-0000-0000-0000-000000000000");
|
|
||||||
this.ordinal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CollectionMonster(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
|
|
||||||
this.collectionId = collectionId;
|
|
||||||
this.monsterId = monsterId;
|
|
||||||
this.ordinal = ordinal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
|
||||||
|
|
||||||
import androidx.room.Embedded;
|
|
||||||
|
|
||||||
public class CollectionWithCount {
|
|
||||||
@Embedded
|
|
||||||
public Collection collection;
|
|
||||||
|
|
||||||
public int monsterCount;
|
|
||||||
|
|
||||||
public CollectionWithCount() {
|
|
||||||
this.collection = new Collection();
|
|
||||||
this.monsterCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CollectionWithCount(Collection collection, int monsterCount) {
|
|
||||||
this.collection = collection;
|
|
||||||
this.monsterCount = monsterCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.room.Entity;
|
|
||||||
import androidx.room.ForeignKey;
|
|
||||||
import androidx.room.Index;
|
|
||||||
import androidx.room.PrimaryKey;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity(
|
|
||||||
tableName = "dashboard_monsters",
|
|
||||||
foreignKeys = @ForeignKey(
|
|
||||||
entity = Monster.class,
|
|
||||||
parentColumns = "id",
|
|
||||||
childColumns = "monster_id",
|
|
||||||
onDelete = ForeignKey.CASCADE
|
|
||||||
),
|
|
||||||
indices = @Index(value = {"monster_id"})
|
|
||||||
)
|
|
||||||
public class DashboardMonster {
|
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
public long id;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "monster_id")
|
|
||||||
public UUID monsterId;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "ordinal", defaultValue = "0")
|
|
||||||
public int ordinal;
|
|
||||||
|
|
||||||
public DashboardMonster() {
|
|
||||||
this.monsterId = UUID.fromString("00000000-0000-0000-0000-000000000000");
|
|
||||||
this.ordinal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DashboardMonster(@NonNull UUID monsterId, int ordinal) {
|
|
||||||
this.monsterId = monsterId;
|
|
||||||
this.ordinal = ordinal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -206,93 +206,6 @@ public class Monster {
|
|||||||
@ColumnInfo(name = "regional_actions", defaultValue = "[]")
|
@ColumnInfo(name = "regional_actions", defaultValue = "[]")
|
||||||
public List<Trait> regionalActions;
|
public List<Trait> regionalActions;
|
||||||
|
|
||||||
@ColumnInfo(name = "source_url", defaultValue = "")
|
|
||||||
public String sourceUrl;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "bonus_actions", defaultValue = "[]")
|
|
||||||
public List<Trait> bonusActions;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "mythic_actions", defaultValue = "[]")
|
|
||||||
public List<Trait> mythicActions;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "legendary_actions_description", defaultValue = "")
|
|
||||||
public String legendaryActionsDescription;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "lair_actions_description", defaultValue = "")
|
|
||||||
public String lairActionsDescription;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "lair_actions_end_note", defaultValue = "")
|
|
||||||
public String lairActionsEndNote;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "regional_actions_description", defaultValue = "")
|
|
||||||
public String regionalActionsDescription;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "regional_actions_end_note", defaultValue = "")
|
|
||||||
public String regionalActionsEndNote;
|
|
||||||
|
|
||||||
@ColumnInfo(name = "mythic_actions_description", defaultValue = "")
|
|
||||||
public String mythicActionsDescription;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "player_name", defaultValue = "")
|
|
||||||
public String playerName;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "background", defaultValue = "")
|
|
||||||
public String background;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "personality_traits", defaultValue = "")
|
|
||||||
public String personalityTraits;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "ideals", defaultValue = "")
|
|
||||||
public String ideals;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "bonds", defaultValue = "")
|
|
||||||
public String bonds;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "flaws", defaultValue = "")
|
|
||||||
public String flaws;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "age", defaultValue = "")
|
|
||||||
public String age;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "height", defaultValue = "")
|
|
||||||
public String height;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "weight", defaultValue = "")
|
|
||||||
public String weight;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "eyes", defaultValue = "")
|
|
||||||
public String eyes;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "skin", defaultValue = "")
|
|
||||||
public String skin;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "hair", defaultValue = "")
|
|
||||||
public String hair;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "appearance", defaultValue = "")
|
|
||||||
public String appearance;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "backstory", defaultValue = "")
|
|
||||||
public String backstory;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@ColumnInfo(name = "allies_and_organizations", defaultValue = "")
|
|
||||||
public String alliesAndOrganizations;
|
|
||||||
|
|
||||||
public Monster() {
|
public Monster() {
|
||||||
id = UUID.randomUUID();
|
id = UUID.randomUUID();
|
||||||
name = "";
|
name = "";
|
||||||
@@ -338,21 +251,6 @@ public class Monster {
|
|||||||
wisdomSavingThrowProficiency = ProficiencyType.NONE;
|
wisdomSavingThrowProficiency = ProficiencyType.NONE;
|
||||||
charismaSavingThrowAdvantage = AdvantageType.NONE;
|
charismaSavingThrowAdvantage = AdvantageType.NONE;
|
||||||
charismaSavingThrowProficiency = ProficiencyType.NONE;
|
charismaSavingThrowProficiency = ProficiencyType.NONE;
|
||||||
playerName = "";
|
|
||||||
background = "";
|
|
||||||
personalityTraits = "";
|
|
||||||
ideals = "";
|
|
||||||
bonds = "";
|
|
||||||
flaws = "";
|
|
||||||
age = "";
|
|
||||||
height = "";
|
|
||||||
weight = "";
|
|
||||||
eyes = "";
|
|
||||||
skin = "";
|
|
||||||
hair = "";
|
|
||||||
appearance = "";
|
|
||||||
backstory = "";
|
|
||||||
alliesAndOrganizations = "";
|
|
||||||
|
|
||||||
|
|
||||||
skills = new HashSet<>();
|
skills = new HashSet<>();
|
||||||
@@ -368,15 +266,6 @@ public class Monster {
|
|||||||
lairActions = new ArrayList<>();
|
lairActions = new ArrayList<>();
|
||||||
legendaryActions = new ArrayList<>();
|
legendaryActions = new ArrayList<>();
|
||||||
regionalActions = new ArrayList<>();
|
regionalActions = new ArrayList<>();
|
||||||
bonusActions = new ArrayList<>();
|
|
||||||
mythicActions = new ArrayList<>();
|
|
||||||
sourceUrl = "";
|
|
||||||
legendaryActionsDescription = "";
|
|
||||||
lairActionsDescription = "";
|
|
||||||
lairActionsEndNote = "";
|
|
||||||
regionalActionsDescription = "";
|
|
||||||
regionalActionsEndNote = "";
|
|
||||||
mythicActionsDescription = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMeta() {
|
public String getMeta() {
|
||||||
@@ -894,22 +783,6 @@ public class Monster {
|
|||||||
return actionDescriptions;
|
return actionDescriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getBonusActionDescriptions() {
|
|
||||||
ArrayList<String> actionDescriptions = new ArrayList<>();
|
|
||||||
for (Trait action : bonusActions) {
|
|
||||||
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
|
|
||||||
}
|
|
||||||
return actionDescriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getMythicActionDescriptions() {
|
|
||||||
ArrayList<String> actionDescriptions = new ArrayList<>();
|
|
||||||
for (Trait action : mythicActions) {
|
|
||||||
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
|
|
||||||
}
|
|
||||||
return actionDescriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getReactionDescriptions() {
|
public List<String> getReactionDescriptions() {
|
||||||
ArrayList<String> actionDescriptions = new ArrayList<>();
|
ArrayList<String> actionDescriptions = new ArrayList<>();
|
||||||
for (Trait action : reactions) {
|
for (Trait action : reactions) {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class SearchResultItem {
|
|
||||||
public enum Type {
|
|
||||||
MONSTER,
|
|
||||||
COLLECTION
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final Type type;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public final Monster monster;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public final Collection collection;
|
|
||||||
|
|
||||||
public SearchResultItem(@NonNull Monster monster) {
|
|
||||||
this.type = Type.MONSTER;
|
|
||||||
this.monster = monster;
|
|
||||||
this.collection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SearchResultItem(@NonNull Collection collection) {
|
|
||||||
this.type = Type.COLLECTION;
|
|
||||||
this.monster = null;
|
|
||||||
this.collection = collection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.ui.collections;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.navigation.NavDirections;
|
|
||||||
import androidx.navigation.Navigation;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter;
|
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class CollectionDetailFragment extends MCFragment {
|
|
||||||
|
|
||||||
private CollectionDetailViewModel mViewModel;
|
|
||||||
private UUID mCollectionId;
|
|
||||||
private DashboardRecyclerViewAdapter mAdapter;
|
|
||||||
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
View root = inflater.inflate(R.layout.fragment_collection_detail, container, false);
|
|
||||||
|
|
||||||
Bundle arguments = getArguments();
|
|
||||||
assert arguments != null;
|
|
||||||
mCollectionId = UUID.fromString(CollectionDetailFragmentArgs.fromBundle(arguments).getCollectionId());
|
|
||||||
|
|
||||||
mViewModel = new ViewModelProvider(this).get(CollectionDetailViewModel.class);
|
|
||||||
|
|
||||||
TextView nameView = root.findViewById(R.id.detail_collection_name);
|
|
||||||
TextView descriptionView = root.findViewById(R.id.detail_collection_description);
|
|
||||||
RecyclerView recyclerView = root.findViewById(R.id.collection_monster_list);
|
|
||||||
FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection);
|
|
||||||
|
|
||||||
if (fab != null) {
|
|
||||||
fab.setOnClickListener(v -> showAddMonsterDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> {
|
|
||||||
if (collection != null) {
|
|
||||||
nameView.setText(collection.name);
|
|
||||||
setTitle(collection.name);
|
|
||||||
if (!TextUtils.isEmpty(collection.description)) {
|
|
||||||
descriptionView.setVisibility(View.VISIBLE);
|
|
||||||
descriptionView.setText(collection.description);
|
|
||||||
} else {
|
|
||||||
descriptionView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter.submitList(monsters);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setupRecyclerView(recyclerView);
|
|
||||||
loadCollectionData();
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadCollectionData() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.getCollection(mCollectionId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(collection -> {
|
|
||||||
if (collection != null) {
|
|
||||||
mViewModel.setCollection(collection);
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
|
|
||||||
mDisposables.add(repository.getMonstersForCollection(mCollectionId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
|
||||||
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396);
|
|
||||||
Context context = requireContext();
|
|
||||||
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
|
||||||
|
|
||||||
mAdapter = new DashboardRecyclerViewAdapter(monster -> {
|
|
||||||
if (monster != null) {
|
|
||||||
navigateToMonsterDetail(monster.id);
|
|
||||||
} else {
|
|
||||||
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
recyclerView.setAdapter(mAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAddMonsterDialog() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.getMonsters()
|
|
||||||
.firstOrError()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(monsters -> {
|
|
||||||
if (monsters.isEmpty()) {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] monsterNames = new String[monsters.size()];
|
|
||||||
for (int i = 0; i < monsters.size(); i++) {
|
|
||||||
monsterNames[i] = monsters.get(i).name;
|
|
||||||
}
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.action_add_monster)
|
|
||||||
.setItems(monsterNames, (dialog, which) -> {
|
|
||||||
Monster selectedMonster = monsters.get(which);
|
|
||||||
addMonsterToCollection(selectedMonster);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addMonsterToCollection(Monster monster) {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.addMonsterToCollection(mCollectionId, monster.id)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Collection collection = mViewModel.getCollection().getValue();
|
|
||||||
String collectionName = collection != null ? collection.name : "";
|
|
||||||
Snackbar.make(
|
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToMonsterDetail(@NonNull UUID monsterId) {
|
|
||||||
NavDirections action = CollectionDetailFragmentDirections.actionCollectionDetailFragmentToNavigationMonster(monsterId.toString());
|
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.collection_detail_menu, menu);
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
|
||||||
addCollectionToDashboard();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCollectionToDashboard() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Collection collection = mViewModel.getCollection().getValue();
|
|
||||||
String collectionName = collection != null ? collection.name : "";
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
mDisposables.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.ui.collections;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class CollectionDetailViewModel extends ViewModel {
|
|
||||||
private final MutableLiveData<Collection> mCollection = new MutableLiveData<>();
|
|
||||||
private final MutableLiveData<List<Monster>> mMonsters = new MutableLiveData<>(new ArrayList<>());
|
|
||||||
|
|
||||||
public CollectionDetailViewModel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Collection> getCollection() {
|
|
||||||
return mCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCollection(Collection collection) {
|
|
||||||
mCollection.setValue(collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<List<Monster>> getMonsters() {
|
|
||||||
return mMonsters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMonsters(List<Monster> monsters) {
|
|
||||||
mMonsters.setValue(monsters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +1,27 @@
|
|||||||
package com.majinnaibu.monstercards.ui.collections;
|
package com.majinnaibu.monstercards.ui.collections;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.TextView;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.navigation.NavDirections;
|
|
||||||
import androidx.navigation.Navigation;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class CollectionsFragment extends MCFragment {
|
public class CollectionsFragment extends MCFragment {
|
||||||
|
|
||||||
@Nullable
|
private CollectionsViewModel collectionsViewModel;
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
@Nullable ViewGroup container,
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
@Nullable Bundle savedInstanceState) {
|
collectionsViewModel = new ViewModelProvider(this).get(CollectionsViewModel.class);
|
||||||
View root = inflater.inflate(R.layout.fragment_collections, container, false);
|
View root = inflater.inflate(R.layout.fragment_collections, container, false);
|
||||||
|
final TextView textView = root.findViewById(R.id.text_collections);
|
||||||
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
|
collectionsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
||||||
if (fab != null) {
|
|
||||||
fab.setOnClickListener(v -> showCreateCollectionDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
RecyclerView recyclerView = root.findViewById(R.id.collection_list);
|
|
||||||
if (recyclerView != null) {
|
|
||||||
setupRecyclerView(recyclerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
|
||||||
Context context = requireContext();
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
|
|
||||||
CollectionsRecyclerViewAdapter adapter = new CollectionsRecyclerViewAdapter(
|
|
||||||
context,
|
|
||||||
repository.getCollectionsWithCount(),
|
|
||||||
collection -> navigateToCollectionDetail(collection.id),
|
|
||||||
collection -> repository.deleteCollection(collection)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(new DisposableCompletableObserver() {
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
|
||||||
Logger.logError(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
|
||||||
|
|
||||||
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
|
|
||||||
recyclerView.addItemDecoration(dividerItemDecoration);
|
|
||||||
|
|
||||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> adapter.deleteItem(position), null));
|
|
||||||
itemTouchHelper.attachToRecyclerView(recyclerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showCreateCollectionDialog() {
|
|
||||||
Context context = requireContext();
|
|
||||||
LinearLayout layout = new LinearLayout(context);
|
|
||||||
layout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
int padding = (int) (16 * getResources().getDisplayMetrics().density);
|
|
||||||
layout.setPadding(padding, padding, padding, padding);
|
|
||||||
|
|
||||||
final EditText nameInput = new EditText(context);
|
|
||||||
nameInput.setHint(R.string.label_collection_name);
|
|
||||||
layout.addView(nameInput);
|
|
||||||
|
|
||||||
final EditText descInput = new EditText(context);
|
|
||||||
descInput.setHint(R.string.label_collection_description);
|
|
||||||
layout.addView(descInput);
|
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.title_new_collection)
|
|
||||||
.setView(layout)
|
|
||||||
.setPositiveButton(R.string.dialog_create, (dialog, which) -> {
|
|
||||||
String name = nameInput.getText().toString().trim();
|
|
||||||
String description = descInput.getText().toString().trim();
|
|
||||||
if (TextUtils.isEmpty(name)) {
|
|
||||||
name = getString(R.string.title_new_collection);
|
|
||||||
}
|
|
||||||
createCollection(name, description);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createCollection(@NonNull String name, @NonNull String description) {
|
|
||||||
Collection collection = new Collection(name, description);
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
repository.saveCollection(collection)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(new DisposableCompletableObserver() {
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(
|
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_collection_created, collection.name),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.setAction("View", v -> navigateToCollectionDetail(collection.id))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
|
||||||
Logger.logError("Error creating collection", e);
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_collection), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToCollectionDetail(@NonNull UUID collectionId) {
|
|
||||||
NavDirections action = CollectionsFragmentDirections.actionNavigationCollectionsToCollectionDetailFragment(collectionId.toString());
|
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.ui.collections;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.R;
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class CollectionsRecyclerViewAdapter extends RecyclerView.Adapter<CollectionsRecyclerViewAdapter.ViewHolder> {
|
|
||||||
private final Context mContext;
|
|
||||||
private final CollectionCallback mOnClick;
|
|
||||||
private final CollectionCallback mOnDelete;
|
|
||||||
private final Flowable<List<CollectionWithCount>> mItemsObservable;
|
|
||||||
private List<CollectionWithCount> mValues;
|
|
||||||
private Disposable mDisposable;
|
|
||||||
|
|
||||||
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(@NonNull View view) {
|
|
||||||
Collection collection = (Collection) view.getTag();
|
|
||||||
if (mOnClick != null) {
|
|
||||||
mOnClick.onCallback(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public CollectionsRecyclerViewAdapter(Context context,
|
|
||||||
Flowable<List<CollectionWithCount>> itemsObservable,
|
|
||||||
CollectionCallback onClick,
|
|
||||||
CollectionCallback onDelete) {
|
|
||||||
mContext = context;
|
|
||||||
mItemsObservable = itemsObservable;
|
|
||||||
mOnClick = onClick;
|
|
||||||
mOnDelete = onDelete;
|
|
||||||
mValues = new ArrayList<>();
|
|
||||||
mDisposable = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.collection_list_item, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
|
||||||
CollectionWithCount item = mValues.get(position);
|
|
||||||
Collection collection = item.collection;
|
|
||||||
holder.mNameView.setText(collection.name);
|
|
||||||
|
|
||||||
String countText;
|
|
||||||
if (item.monsterCount == 1) {
|
|
||||||
countText = mContext.getString(R.string.format_monster_count_one);
|
|
||||||
} else {
|
|
||||||
countText = mContext.getString(R.string.format_monster_count_other, item.monsterCount);
|
|
||||||
}
|
|
||||||
holder.mCountView.setText(countText);
|
|
||||||
holder.mCountView.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(collection.description)) {
|
|
||||||
holder.mDescriptionView.setVisibility(View.VISIBLE);
|
|
||||||
holder.mDescriptionView.setText(collection.description);
|
|
||||||
} else {
|
|
||||||
holder.mDescriptionView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemView.setTag(collection);
|
|
||||||
holder.itemView.setOnClickListener(mOnClickListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mValues.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
|
||||||
super.onAttachedToRecyclerView(recyclerView);
|
|
||||||
mDisposable = mItemsObservable
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(collections -> {
|
|
||||||
mValues = collections;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
|
||||||
super.onDetachedFromRecyclerView(recyclerView);
|
|
||||||
if (mDisposable != null) {
|
|
||||||
mDisposable.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteItem(int position) {
|
|
||||||
if (mOnDelete != null && position >= 0 && position < mValues.size()) {
|
|
||||||
Collection collection = mValues.get(position).collection;
|
|
||||||
mOnDelete.onCallback(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface CollectionCallback {
|
|
||||||
void onCallback(Collection collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
final TextView mNameView;
|
|
||||||
final TextView mDescriptionView;
|
|
||||||
final TextView mCountView;
|
|
||||||
|
|
||||||
ViewHolder(View view) {
|
|
||||||
super(view);
|
|
||||||
mNameView = view.findViewById(R.id.collection_name);
|
|
||||||
mDescriptionView = view.findViewById(R.id.collection_description);
|
|
||||||
mCountView = view.findViewById(R.id.collection_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,78 +3,52 @@ package com.majinnaibu.monstercards.ui.dashboard;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class DashboardFragment extends MCFragment {
|
public class DashboardFragment extends MCFragment {
|
||||||
private DashboardViewModel mViewModel;
|
private DashboardViewModel mViewModel;
|
||||||
private ViewHolder mHolder;
|
private ViewHolder mHolder;
|
||||||
private DashboardRecyclerViewAdapter mAdapter;
|
private DashboardRecyclerViewAdapter mAdapter;
|
||||||
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
@Nullable ViewGroup container,
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
|
mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
|
||||||
View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
|
View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
|
||||||
mHolder = new ViewHolder(root);
|
mHolder = new ViewHolder(root);
|
||||||
|
|
||||||
FloatingActionButton fab = root.findViewById(R.id.fab_add_to_dashboard);
|
|
||||||
if (fab != null) {
|
|
||||||
fab.setOnClickListener(v -> showAddToDashboardOptionsDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
setupRecyclerView(mHolder.list);
|
setupRecyclerView(mHolder.list);
|
||||||
loadDashboardMonsters();
|
|
||||||
|
// TODO: subscribe better
|
||||||
|
getMonsterRepository()
|
||||||
|
.getMonsters()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monsters -> mViewModel.setMonsters(monsters));
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadDashboardMonsters() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.getDashboardMonsters()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||||
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 360);
|
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396);
|
||||||
Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount));
|
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
@@ -91,165 +65,6 @@ public class DashboardFragment extends MCFragment {
|
|||||||
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
|
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
|
||||||
}
|
}
|
||||||
recyclerView.setAdapter(mAdapter);
|
recyclerView.setAdapter(mAdapter);
|
||||||
|
|
||||||
ItemTouchHelper.SimpleCallback touchHelperCallback = new ItemTouchHelper.SimpleCallback(
|
|
||||||
ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT,
|
|
||||||
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
|
||||||
) {
|
|
||||||
@Override
|
|
||||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
|
||||||
int fromPos = viewHolder.getAdapterPosition();
|
|
||||||
int toPos = target.getAdapterPosition();
|
|
||||||
if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) {
|
|
||||||
List<Monster> currentList = new ArrayList<>(mAdapter.getCurrentList());
|
|
||||||
if (fromPos < currentList.size() && toPos < currentList.size()) {
|
|
||||||
Monster moved = currentList.remove(fromPos);
|
|
||||||
currentList.add(toPos, moved);
|
|
||||||
mAdapter.submitList(currentList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
|
||||||
int position = viewHolder.getAdapterPosition();
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
List<Monster> currentList = mAdapter.getCurrentList();
|
|
||||||
if (position < currentList.size()) {
|
|
||||||
Monster monster = currentList.get(position);
|
|
||||||
removeMonsterFromDashboard(monster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
new ItemTouchHelper(touchHelperCallback).attachToRecyclerView(recyclerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAddToDashboardOptionsDialog() {
|
|
||||||
String[] options = new String[]{
|
|
||||||
getString(R.string.action_add_single_monster),
|
|
||||||
getString(R.string.action_add_collection_option),
|
|
||||||
getString(R.string.action_clear_dashboard)
|
|
||||||
};
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.dialog_add_to_dashboard)
|
|
||||||
.setItems(options, (dialog, which) -> {
|
|
||||||
if (which == 0) {
|
|
||||||
showAddMonsterPicker();
|
|
||||||
} else if (which == 1) {
|
|
||||||
showAddCollectionPicker();
|
|
||||||
} else if (which == 2) {
|
|
||||||
clearDashboard();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAddMonsterPicker() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.getMonsters()
|
|
||||||
.firstOrError()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(monsters -> {
|
|
||||||
if (monsters.isEmpty()) {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.no_monsters_available), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] names = new String[monsters.size()];
|
|
||||||
for (int i = 0; i < monsters.size(); i++) {
|
|
||||||
names[i] = monsters.get(i).name;
|
|
||||||
}
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.action_add_single_monster)
|
|
||||||
.setItems(names, (dialog, which) -> {
|
|
||||||
Monster selected = monsters.get(which);
|
|
||||||
addMonsterToDashboard(selected);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAddCollectionPicker() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.getCollections()
|
|
||||||
.firstOrError()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(collections -> {
|
|
||||||
if (collections.isEmpty()) {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] names = new String[collections.size()];
|
|
||||||
for (int i = 0; i < collections.size(); i++) {
|
|
||||||
names[i] = collections.get(i).name;
|
|
||||||
}
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.action_add_collection_option)
|
|
||||||
.setItems(names, (dialog, which) -> {
|
|
||||||
Collection selected = collections.get(which);
|
|
||||||
addCollectionToDashboard(selected);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addMonsterToDashboard(@NonNull Monster monster) {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.addMonsterToDashboard(monster.id)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monster.name), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCollectionToDashboard(@NonNull Collection collection) {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.addCollectionToDashboard(collection.id)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeMonsterFromDashboard(@NonNull Monster monster) {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.removeMonsterFromDashboard(monster.id)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {}, Logger::logError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearDashboard() {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
mDisposables.add(repository.clearDashboard()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_dashboard_cleared), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}, Logger::logError));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void navigateToMonsterDetail(Monster monster) {
|
private void navigateToMonsterDetail(Monster monster) {
|
||||||
@@ -257,33 +72,6 @@ public class DashboardFragment extends MCFragment {
|
|||||||
Navigation.findNavController(requireView()).navigate(action);
|
Navigation.findNavController(requireView()).navigate(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.dashboard_menu, menu);
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.menu_action_add_single_monster) {
|
|
||||||
showAddMonsterPicker();
|
|
||||||
return true;
|
|
||||||
} else if (item.getItemId() == R.id.menu_action_add_collection_option) {
|
|
||||||
showAddCollectionPicker();
|
|
||||||
return true;
|
|
||||||
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
|
||||||
clearDashboard();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
mDisposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
final RecyclerView list;
|
final RecyclerView list;
|
||||||
|
|
||||||
@@ -291,4 +79,5 @@ public class DashboardFragment extends MCFragment {
|
|||||||
list = root.findViewById(R.id.list);
|
list = root.findViewById(R.id.list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, Dashboard
|
|||||||
};
|
};
|
||||||
private final ItemCallback mOnClick;
|
private final ItemCallback mOnClick;
|
||||||
|
|
||||||
public DashboardRecyclerViewAdapter(ItemCallback onClick) {
|
protected DashboardRecyclerViewAdapter(ItemCallback onClick) {
|
||||||
super(DIFF_CALLBACK);
|
super(DIFF_CALLBACK);
|
||||||
mOnClick = onClick;
|
mOnClick = onClick;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
package com.majinnaibu.monstercards.ui.library;
|
package com.majinnaibu.monstercards.ui.library;
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.navigation.NavController;
|
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
@@ -24,13 +16,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.majinnaibu.monstercards.MainActivity;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
|
||||||
import com.majinnaibu.monstercards.importers.Open5eImporter;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentDirections;
|
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
@@ -45,7 +33,6 @@ public class LibraryFragment extends MCFragment {
|
|||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
setHasOptionsMenu(true);
|
|
||||||
View root = inflater.inflate(R.layout.fragment_library, container, false);
|
View root = inflater.inflate(R.layout.fragment_library, container, false);
|
||||||
|
|
||||||
FloatingActionButton fab = root.findViewById(R.id.fab);
|
FloatingActionButton fab = root.findViewById(R.id.fab);
|
||||||
@@ -59,62 +46,6 @@ public class LibraryFragment extends MCFragment {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.library_menu, menu);
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.menu_action_import_from_url) {
|
|
||||||
showImportUrlDialog();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showImportUrlDialog() {
|
|
||||||
Context context = requireContext();
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(R.string.dialog_import_url_title);
|
|
||||||
builder.setMessage(R.string.dialog_import_url_message);
|
|
||||||
|
|
||||||
final EditText input = new EditText(context);
|
|
||||||
input.setHint(R.string.dialog_import_url_hint);
|
|
||||||
input.setSingleLine(true);
|
|
||||||
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
if (clipboard != null && clipboard.hasPrimaryClip()) {
|
|
||||||
ClipData clip = clipboard.getPrimaryClip();
|
|
||||||
if (clip != null && clip.getItemCount() > 0) {
|
|
||||||
CharSequence clipText = clip.getItemAt(0).getText();
|
|
||||||
if (clipText != null) {
|
|
||||||
String clipStr = clipText.toString().trim();
|
|
||||||
if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) {
|
|
||||||
input.setText(clipStr);
|
|
||||||
input.selectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setView(input);
|
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.dialog_import, (dialog, which) -> {
|
|
||||||
String urlOrId = input.getText().toString().trim();
|
|
||||||
if (!urlOrId.isEmpty()) {
|
|
||||||
if (getActivity() instanceof MainActivity) {
|
|
||||||
((MainActivity) getActivity()).importMonsterFromInputAndNavigate(urlOrId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> dialog.cancel());
|
|
||||||
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
MonsterRepository repository = this.getMonsterRepository();
|
MonsterRepository repository = this.getMonsterRepository();
|
||||||
@@ -161,16 +92,23 @@ public class LibraryFragment extends MCFragment {
|
|||||||
new DisposableCompletableObserver() {
|
new DisposableCompletableObserver() {
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
navigateToEditMonster(monster.id);
|
View view = getView();
|
||||||
|
assert view != null;
|
||||||
|
Snackbar.make(
|
||||||
|
view,
|
||||||
|
getString(R.string.snackbar_monster_created, monster.name),
|
||||||
|
Snackbar.LENGTH_LONG)
|
||||||
|
.setAction("Action", (_view) -> navigateToMonsterDetail(monster.id))
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
||||||
Logger.logError("Error creating monster", e);
|
Logger.logError("Error creating monster", e);
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
assert view != null;
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
|
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG)
|
||||||
}
|
.setAction("Action", null).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -180,12 +118,4 @@ public class LibraryFragment extends MCFragment {
|
|||||||
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
|
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
Navigation.findNavController(requireView()).navigate(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void navigateToEditMonster(@NonNull UUID monsterId) {
|
|
||||||
NavController navController = Navigation.findNavController(requireView());
|
|
||||||
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
|
|
||||||
navController.navigate(action);
|
|
||||||
action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
|
|
||||||
navController.navigate(action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
package com.majinnaibu.monstercards.ui.monster;
|
package com.majinnaibu.monstercards.ui.monster;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
import com.majinnaibu.monstercards.exporters.Open5eExporter;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
@@ -26,17 +18,14 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
@@ -44,10 +33,7 @@ import com.majinnaibu.monstercards.utils.Logger;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
|
||||||
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
|
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class MonsterDetailFragment extends MCFragment {
|
public class MonsterDetailFragment extends MCFragment {
|
||||||
private ViewHolder mHolder;
|
private ViewHolder mHolder;
|
||||||
@@ -186,150 +172,10 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
Logger.logWTF("monsterId cannot be null.");
|
Logger.logWTF("monsterId cannot be null.");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (item.getItemId() == R.id.menu_action_add_to_collection) {
|
|
||||||
showAddToCollectionDialog();
|
|
||||||
return true;
|
|
||||||
} else if (item.getItemId() == R.id.menu_action_add_to_dashboard) {
|
|
||||||
addCurrentMonsterToDashboard();
|
|
||||||
return true;
|
|
||||||
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
|
||||||
shareCurrentMonster();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void shareCurrentMonster() {
|
|
||||||
Monster monster = mViewModel.getMonster();
|
|
||||||
if (monster == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Open5eExporter exporter = new Open5eExporter();
|
|
||||||
String jsonCard = exporter.exportMonster(monster);
|
|
||||||
|
|
||||||
Context context = requireContext();
|
|
||||||
String safeName = (monster.name != null && !monster.name.trim().isEmpty())
|
|
||||||
? monster.name.replaceAll("[^a-zA-Z0-9._-]", "_")
|
|
||||||
: "monster";
|
|
||||||
File file = new File(context.getCacheDir(), safeName + ".card");
|
|
||||||
|
|
||||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
|
||||||
out.write(jsonCard.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
context.getPackageName() + ".fileprovider",
|
|
||||||
file
|
|
||||||
);
|
|
||||||
|
|
||||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
|
||||||
shareIntent.setType("application/octet-stream");
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, jsonCard);
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, monster.name);
|
|
||||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
|
|
||||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.action_share_monster)));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.logError("Failed to share monster", e);
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, R.string.failed_to_share_monster, Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCurrentMonsterToDashboard() {
|
|
||||||
UUID monsterId = mViewModel.getId().getValue();
|
|
||||||
if (monsterId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
repository.addMonsterToDashboard(monsterId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
String monsterName = mViewModel.getName().getValue();
|
|
||||||
Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monsterName), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}, Logger::logError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAddToCollectionDialog() {
|
|
||||||
UUID monsterId = mViewModel.getId().getValue();
|
|
||||||
if (monsterId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
repository.getCollections()
|
|
||||||
.firstOrError()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(new DisposableSingleObserver<List<Collection>>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull List<Collection> collections) {
|
|
||||||
if (collections.isEmpty()) {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] collectionNames = new String[collections.size()];
|
|
||||||
for (int i = 0; i < collections.size(); i++) {
|
|
||||||
collectionNames[i] = collections.get(i).name;
|
|
||||||
}
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.title_select_collection)
|
|
||||||
.setItems(collectionNames, (dialog, which) -> {
|
|
||||||
Collection selectedCollection = collections.get(which);
|
|
||||||
addMonsterToCollection(selectedCollection, monsterId);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
|
||||||
.show();
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
|
||||||
Logger.logError(e);
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addMonsterToCollection(@NonNull Collection collection, @NonNull UUID monsterId) {
|
|
||||||
MonsterRepository repository = getMonsterRepository();
|
|
||||||
repository.addMonsterToCollection(collection.id, monsterId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(new DisposableCompletableObserver() {
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
View view = getView();
|
|
||||||
if (view != null) {
|
|
||||||
String monsterName = mViewModel.getName().getValue();
|
|
||||||
Snackbar.make(
|
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
|
||||||
Logger.logError(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
final TextView name;
|
final TextView name;
|
||||||
final TextView meta;
|
final TextView meta;
|
||||||
|
|||||||
@@ -181,10 +181,6 @@ public class MonsterDetailViewModel extends ViewModel {
|
|||||||
return mMonsterId;
|
return mMonsterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Monster getMonster() {
|
|
||||||
return mMonster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMonster(@NonNull Monster monster) {
|
public void setMonster(@NonNull Monster monster) {
|
||||||
mMonster = monster;
|
mMonster = monster;
|
||||||
mAbilities.setValue(mMonster.getAbilityDescriptions());
|
mAbilities.setValue(mMonster.getAbilityDescriptions());
|
||||||
|
|||||||
@@ -162,13 +162,12 @@ public class MonsterImportFragment extends MCFragment {
|
|||||||
repository.addMonster(monster).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new DisposableCompletableObserver() {
|
repository.addMonster(monster).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new DisposableCompletableObserver() {
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
if (getView() != null) {
|
Snackbar.make(
|
||||||
NavController navController = Navigation.findNavController(requireView());
|
mHolder.root,
|
||||||
NavDirections toLibraryAction = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary();
|
getString(R.string.snackbar_monster_created, monster.name),
|
||||||
navController.navigate(toLibraryAction);
|
Snackbar.LENGTH_LONG)
|
||||||
NavDirections toMonsterDetailAction = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monster.id.toString());
|
.setAction("Action", (_view) -> navigateToEditMonster(monster.id))
|
||||||
navController.navigate(toMonsterDetailAction);
|
.show();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -185,6 +184,17 @@ public class MonsterImportFragment extends MCFragment {
|
|||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void navigateToEditMonster(@NonNull UUID monsterId) {
|
||||||
|
NavController navController = Navigation.findNavController(requireView());
|
||||||
|
NavDirections action;
|
||||||
|
action = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary();
|
||||||
|
navController.navigate(action);
|
||||||
|
action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
|
||||||
|
navController.navigate(action);
|
||||||
|
action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
|
||||||
|
navController.navigate(action);
|
||||||
|
}
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
final View root;
|
final View root;
|
||||||
final TextView name;
|
final TextView name;
|
||||||
|
|||||||
@@ -9,14 +9,11 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.navigation.NavDirections;
|
|
||||||
import androidx.navigation.Navigation;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
|
|
||||||
public class SearchFragment extends MCFragment {
|
public class SearchFragment extends MCFragment {
|
||||||
@@ -25,17 +22,7 @@ public class SearchFragment extends MCFragment {
|
|||||||
ViewGroup container, Bundle savedInstanceState) {
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
View root = inflater.inflate(R.layout.fragment_search, container, false);
|
View root = inflater.inflate(R.layout.fragment_search, container, false);
|
||||||
MonsterRepository repository = this.getMonsterRepository();
|
MonsterRepository repository = this.getMonsterRepository();
|
||||||
SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, item -> {
|
SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, null);
|
||||||
if (item != null) {
|
|
||||||
if (item.type == SearchResultItem.Type.MONSTER && item.monster != null) {
|
|
||||||
NavDirections action = SearchFragmentDirections.actionNavigationSearchToNavigationMonster(item.monster.id.toString());
|
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
|
||||||
} else if (item.type == SearchResultItem.Type.COLLECTION && item.collection != null) {
|
|
||||||
NavDirections action = SearchFragmentDirections.actionNavigationSearchToCollectionDetailFragment(item.collection.id.toString());
|
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
|
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
|
||||||
assert recyclerView != null;
|
assert recyclerView != null;
|
||||||
setupRecyclerView(recyclerView, adapter);
|
setupRecyclerView(recyclerView, adapter);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.majinnaibu.monstercards.ui.search;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -11,10 +10,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,7 +23,7 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
|
|||||||
private final MonsterRepository mRepository;
|
private final MonsterRepository mRepository;
|
||||||
private final ItemCallback mOnClickHandler;
|
private final ItemCallback mOnClickHandler;
|
||||||
private String mSearchText;
|
private String mSearchText;
|
||||||
private List<SearchResultItem> mValues;
|
private List<Monster> mValues;
|
||||||
private Disposable mSubscriptionHandler;
|
private Disposable mSubscriptionHandler;
|
||||||
|
|
||||||
public SearchResultsRecyclerViewAdapter(MonsterRepository repository,
|
public SearchResultsRecyclerViewAdapter(MonsterRepository repository,
|
||||||
@@ -46,9 +42,9 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
|
|||||||
mSubscriptionHandler.dispose();
|
mSubscriptionHandler.dispose();
|
||||||
}
|
}
|
||||||
mSearchText = searchText;
|
mSearchText = searchText;
|
||||||
Flowable<List<SearchResultItem>> resultsFlowable = mRepository.searchAll(mSearchText);
|
Flowable<List<Monster>> foundMonsters = mRepository.searchMonsters(mSearchText);
|
||||||
mSubscriptionHandler = resultsFlowable.subscribe(results -> {
|
mSubscriptionHandler = foundMonsters.subscribe(monsters -> {
|
||||||
mValues = results;
|
mValues = monsters;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
},
|
},
|
||||||
throwable -> Logger.logError("Error performing search", throwable));
|
throwable -> Logger.logError("Error performing search", throwable));
|
||||||
@@ -58,52 +54,18 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.search_result_list_item, parent, false);
|
.inflate(R.layout.monster_list_content, parent, false);
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
|
||||||
SearchResultItem item = mValues.get(position);
|
Monster monster = mValues.get(position);
|
||||||
if (item.type == SearchResultItem.Type.MONSTER && item.monster != null) {
|
holder.mContentView.setText(monster.name);
|
||||||
Monster monster = item.monster;
|
holder.itemView.setTag(monster);
|
||||||
holder.mTitleView.setText(monster.name);
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
if (!StringHelper.isNullOrEmpty(monster.alignment)) {
|
|
||||||
sb.append(monster.alignment);
|
|
||||||
}
|
|
||||||
String cr = monster.getChallengeRatingDescription();
|
|
||||||
if (!StringHelper.isNullOrEmpty(cr)) {
|
|
||||||
if (sb.length() > 0) {
|
|
||||||
sb.append(", ");
|
|
||||||
}
|
|
||||||
sb.append("CR ").append(cr);
|
|
||||||
}
|
|
||||||
if (sb.length() > 0) {
|
|
||||||
holder.mSubtitleView.setText(sb.toString());
|
|
||||||
holder.mSubtitleView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
holder.mSubtitleView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
holder.mIconView.setVisibility(View.GONE);
|
|
||||||
} else if (item.type == SearchResultItem.Type.COLLECTION && item.collection != null) {
|
|
||||||
Collection collection = item.collection;
|
|
||||||
holder.mTitleView.setText(collection.name);
|
|
||||||
|
|
||||||
if (!StringHelper.isNullOrEmpty(collection.description)) {
|
|
||||||
holder.mSubtitleView.setText(collection.description);
|
|
||||||
holder.mSubtitleView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
holder.mSubtitleView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
holder.mIconView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemView.setTag(item);
|
|
||||||
holder.itemView.setOnClickListener(view -> {
|
holder.itemView.setOnClickListener(view -> {
|
||||||
if (mOnClickHandler != null) {
|
if (mOnClickHandler != null) {
|
||||||
mOnClickHandler.onItem(item);
|
mOnClickHandler.onItem(monster);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,19 +76,15 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface ItemCallback {
|
public interface ItemCallback {
|
||||||
void onItem(SearchResultItem item);
|
void onItem(Monster monster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
final TextView mTitleView;
|
final TextView mContentView;
|
||||||
final TextView mSubtitleView;
|
|
||||||
final ImageView mIconView;
|
|
||||||
|
|
||||||
ViewHolder(View view) {
|
ViewHolder(View view) {
|
||||||
super(view);
|
super(view);
|
||||||
mTitleView = view.findViewById(R.id.title);
|
mContentView = view.findViewById(R.id.content);
|
||||||
mSubtitleView = view.findViewById(R.id.subtitle);
|
|
||||||
mIconView = view.findViewById(R.id.icon);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.47 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.23 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/collection_name"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceListItem"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/collection_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/collection_description"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceListItemSecondary"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.collections.CollectionDetailFragment">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/collection_header"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/detail_collection_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/detail_collection_description"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/header_divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?android:attr/listDivider"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/collection_header" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/collection_monster_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="@dimen/padding_normal"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/header_divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
tools:listitem="@layout/card_monster" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab_add_monster_to_collection"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="@dimen/fab_margin"
|
|
||||||
android:contentDescription="@string/action_add_monster"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_input_add"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -6,26 +6,17 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.collections.CollectionsFragment">
|
tools:context=".ui.collections.CollectionsFragment">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<TextView
|
||||||
android:id="@+id/collection_list"
|
android:id="@+id/text_collections"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
app:layoutManager="LinearLayoutManager"
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="20sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:listitem="@layout/collection_list_item" />
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab_add_collection"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="@dimen/fab_margin"
|
|
||||||
android:contentDescription="@string/action_add_collection"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_input_add"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -20,16 +20,4 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:listitem="@layout/card_monster" />
|
tools:listitem="@layout/card_monster" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab_add_to_dashboard"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="@dimen/fab_margin"
|
|
||||||
android:contentDescription="@string/action_add_to_dashboard"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_input_add"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="?attr/selectableItemBackground">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="?attr/textAppearanceListItem"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/subtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceListItemSecondary"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:contentDescription="@string/title_collections"
|
|
||||||
android:src="@drawable/ic_collections_black_24dp"
|
|
||||||
app:tint="?android:attr/textColorSecondary"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_add_collection_to_dashboard"
|
|
||||||
android:title="@string/action_add_collection_to_dashboard"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_add_single_monster"
|
|
||||||
android:title="@string/action_add_single_monster"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_add_collection_option"
|
|
||||||
android:title="@string/action_add_collection_option"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_clear_dashboard"
|
|
||||||
android:title="@string/action_clear_dashboard"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_import_from_url"
|
|
||||||
android:title="@string/action_import_from_url"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
|
||||||
@@ -6,21 +6,5 @@
|
|||||||
android:id="@+id/menu_action_edit_monster"
|
android:id="@+id/menu_action_edit_monster"
|
||||||
android:icon="@drawable/ic_edit_24"
|
android:icon="@drawable/ic_edit_24"
|
||||||
android:title="@string/action_edit"
|
android:title="@string/action_edit"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_share_monster"
|
|
||||||
android:icon="@drawable/ic_share_24"
|
|
||||||
android:title="@string/action_share_monster"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_add_to_collection"
|
|
||||||
android:title="@string/action_add_to_collection"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_action_add_to_dashboard"
|
|
||||||
android:title="@string/action_add_to_dashboard"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_navigation_search_to_navigation_monster"
|
android:id="@+id/action_navigation_search_to_navigation_monster"
|
||||||
app:destination="@id/navigation_monster" />
|
app:destination="@id/navigation_monster" />
|
||||||
<action
|
|
||||||
android:id="@+id/action_navigation_search_to_collectionDetailFragment"
|
|
||||||
app:destination="@id/collectionDetailFragment" />
|
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_dashboard"
|
android:id="@+id/navigation_dashboard"
|
||||||
@@ -34,21 +31,6 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_navigation_collections_to_navigation_monster"
|
android:id="@+id/action_navigation_collections_to_navigation_monster"
|
||||||
app:destination="@id/navigation_monster" />
|
app:destination="@id/navigation_monster" />
|
||||||
<action
|
|
||||||
android:id="@+id/action_navigation_collections_to_collectionDetailFragment"
|
|
||||||
app:destination="@id/collectionDetailFragment" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/collectionDetailFragment"
|
|
||||||
android:name="com.majinnaibu.monstercards.ui.collections.CollectionDetailFragment"
|
|
||||||
android:label="@string/title_collection_details"
|
|
||||||
tools:layout="@layout/fragment_collection_detail">
|
|
||||||
<argument
|
|
||||||
android:name="collection_id"
|
|
||||||
app:argType="string" />
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_collectionDetailFragment_to_navigation_monster"
|
|
||||||
app:destination="@id/navigation_monster" />
|
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_library"
|
android:id="@+id/navigation_library"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="action_add_ability">Add Ability</string>
|
<string name="action_add_ability">Add Ability</string>
|
||||||
<string name="action_add_collection">Add collection</string>
|
|
||||||
<string name="action_add_to_collection">Add to Collection</string>
|
|
||||||
<string name="action_add_condition_immunity">Add Condition</string>
|
<string name="action_add_condition_immunity">Add Condition</string>
|
||||||
<string name="action_add_damage_type">Add Damage Type</string>
|
<string name="action_add_damage_type">Add Damage Type</string>
|
||||||
<string name="action_add_item">Add Item</string>
|
<string name="action_add_item">Add Item</string>
|
||||||
@@ -139,36 +137,4 @@
|
|||||||
<string name="title_monsterDetails_fmt">%1$s Details</string>
|
<string name="title_monsterDetails_fmt">%1$s Details</string>
|
||||||
<string name="title_search">Search</string>
|
<string name="title_search">Search</string>
|
||||||
<string name="wisdom_abbreviation">WIS</string>
|
<string name="wisdom_abbreviation">WIS</string>
|
||||||
<string name="title_new_collection">New Collection</string>
|
|
||||||
<string name="title_collection_details">Collection Details</string>
|
|
||||||
<string name="title_select_collection">Select Collection</string>
|
|
||||||
<string name="label_collection_name">Collection Name</string>
|
|
||||||
<string name="label_collection_description">Description (optional)</string>
|
|
||||||
<string name="dialog_create">Create</string>
|
|
||||||
<string name="dialog_cancel">Cancel</string>
|
|
||||||
<string name="snackbar_collection_created">Collection %1$s created</string>
|
|
||||||
<string name="snackbar_monster_added_to_collection">Added %1$s to %2$s</string>
|
|
||||||
<string name="snackbar_failed_to_create_collection">Failed to create collection</string>
|
|
||||||
<string name="no_collections_available">No collections available. Create one first!</string>
|
|
||||||
<string name="format_monster_count_one">1 monster</string>
|
|
||||||
<string name="format_monster_count_other">%d monsters</string>
|
|
||||||
<string name="action_add_to_dashboard">Add to Dashboard</string>
|
|
||||||
<string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string>
|
|
||||||
<string name="action_add_single_monster">Add Monster</string>
|
|
||||||
<string name="action_add_collection_option">Add Collection</string>
|
|
||||||
<string name="action_clear_dashboard">Clear Dashboard</string>
|
|
||||||
<string name="dialog_add_to_dashboard">Add to Dashboard</string>
|
|
||||||
<string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string>
|
|
||||||
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
|
|
||||||
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
|
|
||||||
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
|
|
||||||
<string name="action_import_from_url">Import URL...</string>
|
|
||||||
<string name="dialog_import_url_title">Import Entity or Character</string>
|
|
||||||
<string name="dialog_import_url_message">Enter or paste a D&D Beyond URL, Character ID, or Open5e JSON payload:</string>
|
|
||||||
<string name="dialog_import_url_hint">e.g. D&D Beyond URL or Open5e JSON</string>
|
|
||||||
<string name="dialog_import">Import</string>
|
|
||||||
<string name="toast_importing_url">Importing entity...</string>
|
|
||||||
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
|
|
||||||
<string name="action_share_monster">Share / Export Monster (.card)</string>
|
|
||||||
<string name="failed_to_share_monster">Failed to share monster</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<cache-path name="shared_cards" path="." />
|
|
||||||
<external-cache-path name="shared_cards_ext" path="." />
|
|
||||||
</paths>
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.majinnaibu.monstercards.init;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavDestination;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class FlipperInitializer {
|
||||||
|
|
||||||
|
public static void init(Context ctx) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendNavigationEvent(NavController controller, NavDestination destination, Bundle arguments) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.exporters;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
|
||||||
import com.majinnaibu.monstercards.models.Language;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
import com.majinnaibu.monstercards.models.Skill;
|
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class Open5eExporterTest {
|
|
||||||
|
|
||||||
private Open5eExporter exporter;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
exporter = new Open5eExporter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExportMonster_BasicFieldsAndEnvelope() {
|
|
||||||
Monster monster = new Monster();
|
|
||||||
monster.id = UUID.fromString("e5b72e61-a83d-4c3d-b4b1-e28a5a41c101");
|
|
||||||
monster.name = "Goblin";
|
|
||||||
monster.size = "Small";
|
|
||||||
monster.type = "Humanoid";
|
|
||||||
monster.subtype = "goblinoid";
|
|
||||||
monster.alignment = "neutral evil";
|
|
||||||
monster.strengthScore = 8;
|
|
||||||
monster.dexterityScore = 14;
|
|
||||||
monster.constitutionScore = 10;
|
|
||||||
monster.intelligenceScore = 10;
|
|
||||||
monster.wisdomScore = 8;
|
|
||||||
monster.charismaScore = 8;
|
|
||||||
monster.walkSpeed = 30;
|
|
||||||
|
|
||||||
monster.playerName = "Player One";
|
|
||||||
monster.background = "Criminal";
|
|
||||||
monster.personalityTraits = "Always cautious";
|
|
||||||
|
|
||||||
monster.languages.add(new Language("Common", true));
|
|
||||||
monster.languages.add(new Language("Goblin", true));
|
|
||||||
|
|
||||||
monster.skills.add(new Skill("Stealth", AbilityScore.DEXTERITY));
|
|
||||||
|
|
||||||
monster.abilities.add(new Trait("Nimble Escape", "Disengage or Hide as bonus action."));
|
|
||||||
monster.actions.add(new Trait("Scimitar", "+4 to hit, 1d6+2 slashing."));
|
|
||||||
|
|
||||||
String json = exporter.exportMonster(monster);
|
|
||||||
assertNotNull(json);
|
|
||||||
|
|
||||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
|
||||||
assertEquals("open5e", root.get("ruleset_id").getAsString());
|
|
||||||
assertEquals("character", root.get("entity_type").getAsString());
|
|
||||||
assertEquals("Goblin", root.get("display_name").getAsString());
|
|
||||||
assertEquals("e5b72e61-a83d-4c3d-b4b1-e28a5a41c101", root.get("uuid").getAsString());
|
|
||||||
|
|
||||||
assertTrue(root.has("properties"));
|
|
||||||
JsonObject props = root.getAsJsonObject("properties");
|
|
||||||
assertEquals("Goblin", props.get("name").getAsString());
|
|
||||||
assertEquals("Small", props.get("size").getAsString());
|
|
||||||
assertEquals("Player One", props.get("player_name").getAsString());
|
|
||||||
assertEquals("Criminal", props.get("background").getAsString());
|
|
||||||
|
|
||||||
JsonObject abilities = props.getAsJsonObject("abilities");
|
|
||||||
assertEquals(8, abilities.get("strength").getAsInt());
|
|
||||||
assertEquals(14, abilities.get("dexterity").getAsInt());
|
|
||||||
|
|
||||||
assertTrue(props.has("skills"));
|
|
||||||
assertTrue(props.has("languages"));
|
|
||||||
assertTrue(props.has("traits"));
|
|
||||||
assertTrue(props.has("actions"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
public class DnDBeyondImporterTest {
|
|
||||||
|
|
||||||
private DnDBeyondImporter importer;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
importer = new DnDBeyondImporter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCanImport_UrlAndNumericIdAndJson() {
|
|
||||||
assertTrue(importer.canImport("https://www.dndbeyond.com/characters/49074997/s7sLyX"));
|
|
||||||
assertTrue(importer.canImport("https://www.dndbeyond.com/characters/49074997"));
|
|
||||||
assertTrue(importer.canImport("49074997"));
|
|
||||||
assertTrue(importer.canImport(" 49074997 "));
|
|
||||||
assertFalse(importer.canImport("invalid url"));
|
|
||||||
assertFalse(importer.canImport(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExtractCharacterId() {
|
|
||||||
assertEquals("49074997", DnDBeyondImporter.extractCharacterId("https://www.dndbeyond.com/characters/49074997/s7sLyX"));
|
|
||||||
assertEquals("49074997", DnDBeyondImporter.extractCharacterId("https://www.dndbeyond.com/characters/49074997"));
|
|
||||||
assertEquals("49074997", DnDBeyondImporter.extractCharacterId("49074997"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParseSampleCharacterJson() throws Exception {
|
|
||||||
File sampleFile = new File("../docs/dndbeyond-character-sample.json");
|
|
||||||
if (!sampleFile.exists()) {
|
|
||||||
sampleFile = new File("../../docs/dndbeyond-character-sample.json");
|
|
||||||
}
|
|
||||||
assertTrue("Sample character JSON file should exist", sampleFile.exists());
|
|
||||||
|
|
||||||
String json = new String(Files.readAllBytes(sampleFile.toPath()));
|
|
||||||
assertTrue(importer.canImport(json));
|
|
||||||
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals("Joy", monster.name);
|
|
||||||
assertEquals(12, monster.strengthScore);
|
|
||||||
assertEquals(15, monster.dexterityScore);
|
|
||||||
assertEquals(14, monster.constitutionScore);
|
|
||||||
assertEquals(10, monster.intelligenceScore);
|
|
||||||
assertEquals(13, monster.wisdomScore);
|
|
||||||
assertEquals(17, monster.charismaScore);
|
|
||||||
|
|
||||||
assertTrue(monster.hasCustomHP);
|
|
||||||
assertEquals("28 (5d8)", monster.customHPDescription);
|
|
||||||
assertEquals(5, monster.hitDice);
|
|
||||||
|
|
||||||
Monster delegatedMonster = MonsterImportHelper.fromJSON(json);
|
|
||||||
assertNotNull(delegatedMonster);
|
|
||||||
assertEquals("Joy", delegatedMonster.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.exporters.Open5eExporter;
|
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
public class Open5eImporterTest {
|
|
||||||
|
|
||||||
private Open5eImporter importer;
|
|
||||||
private Open5eExporter exporter;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
importer = new Open5eImporter();
|
|
||||||
exporter = new Open5eExporter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParseGoblinSampleJson() throws Exception {
|
|
||||||
File sampleFile = new File("/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/examples/character/goblin.json");
|
|
||||||
if (!sampleFile.exists()) {
|
|
||||||
sampleFile = new File("../../../CharacterDataFiles/rulesets/open5e/examples/character/goblin.json");
|
|
||||||
}
|
|
||||||
if (!sampleFile.exists()) {
|
|
||||||
sampleFile = new File("../../CharacterDataFiles/rulesets/open5e/examples/character/goblin.json");
|
|
||||||
}
|
|
||||||
assertTrue("Goblin sample JSON file should exist", sampleFile.exists());
|
|
||||||
|
|
||||||
String json = new String(Files.readAllBytes(sampleFile.toPath()));
|
|
||||||
assertTrue(importer.canImport(json));
|
|
||||||
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals("Goblin", monster.name);
|
|
||||||
assertEquals("small", monster.size);
|
|
||||||
assertEquals("Humanoid", monster.type);
|
|
||||||
assertEquals("goblinoid", monster.subtype);
|
|
||||||
assertEquals("neutral evil", monster.alignment);
|
|
||||||
|
|
||||||
assertEquals(8, monster.strengthScore);
|
|
||||||
assertEquals(14, monster.dexterityScore);
|
|
||||||
assertEquals(10, monster.constitutionScore);
|
|
||||||
assertEquals(10, monster.intelligenceScore);
|
|
||||||
assertEquals(8, monster.wisdomScore);
|
|
||||||
assertEquals(8, monster.charismaScore);
|
|
||||||
|
|
||||||
assertEquals(30, monster.walkSpeed);
|
|
||||||
assertEquals(2, monster.hitDice);
|
|
||||||
|
|
||||||
assertEquals(1, monster.abilities.size());
|
|
||||||
assertEquals("Nimble Escape", monster.abilities.get(0).name);
|
|
||||||
|
|
||||||
assertEquals(2, monster.actions.size());
|
|
||||||
assertEquals("Scimitar", monster.actions.get(0).name);
|
|
||||||
assertEquals("Shortbow", monster.actions.get(1).name);
|
|
||||||
|
|
||||||
Monster delegatedMonster = MonsterImportHelper.fromJSON(json);
|
|
||||||
assertNotNull(delegatedMonster);
|
|
||||||
assertEquals("Goblin", delegatedMonster.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRoundTrip_ExportAndImport() throws Exception {
|
|
||||||
Monster monster = new Monster();
|
|
||||||
monster.name = "Custom Dragon";
|
|
||||||
monster.size = "Huge";
|
|
||||||
monster.type = "Dragon";
|
|
||||||
monster.strengthScore = 23;
|
|
||||||
monster.dexterityScore = 10;
|
|
||||||
monster.constitutionScore = 21;
|
|
||||||
monster.intelligenceScore = 14;
|
|
||||||
monster.wisdomScore = 13;
|
|
||||||
monster.charismaScore = 17;
|
|
||||||
monster.playerName = "DM";
|
|
||||||
monster.background = "Ancient";
|
|
||||||
|
|
||||||
String open5eJson = exporter.exportMonster(monster);
|
|
||||||
assertTrue(importer.canImport(open5eJson));
|
|
||||||
|
|
||||||
Monster importedMonster = importer.parse(open5eJson);
|
|
||||||
assertNotNull(importedMonster);
|
|
||||||
assertEquals("Custom Dragon", importedMonster.name);
|
|
||||||
assertEquals("Huge", importedMonster.size);
|
|
||||||
assertEquals("Dragon", importedMonster.type);
|
|
||||||
assertEquals(23, importedMonster.strengthScore);
|
|
||||||
assertEquals(21, importedMonster.constitutionScore);
|
|
||||||
assertEquals("DM", importedMonster.playerName);
|
|
||||||
assertEquals("Ancient", importedMonster.background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
package com.majinnaibu.monstercards.importers;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
public class TetraCubeMonsterImporterTest {
|
|
||||||
|
|
||||||
private final TetraCubeMonsterImporter importer = new TetraCubeMonsterImporter();
|
|
||||||
|
|
||||||
private static final String SAMPLE_TETRACUBE_JSON = "{"
|
|
||||||
+ "\"name\": \"Goblin\","
|
|
||||||
+ "\"size\": \"Small\","
|
|
||||||
+ "\"type\": \"humanoid\","
|
|
||||||
+ "\"tag\": \"goblinoid\","
|
|
||||||
+ "\"alignment\": \"neutral evil\","
|
|
||||||
+ "\"hitDice\": 2,"
|
|
||||||
+ "\"armorName\": \"leather\","
|
|
||||||
+ "\"shieldBonus\": 1,"
|
|
||||||
+ "\"speed\": 30,"
|
|
||||||
+ "\"strPoints\": 8,"
|
|
||||||
+ "\"dexPoints\": 14,"
|
|
||||||
+ "\"conPoints\": 10,"
|
|
||||||
+ "\"intPoints\": 10,"
|
|
||||||
+ "\"wisPoints\": 8,"
|
|
||||||
+ "\"chaPoints\": 8,"
|
|
||||||
+ "\"cr\": \"1/4\""
|
|
||||||
+ "}";
|
|
||||||
|
|
||||||
private static final String ANCIENT_BLACK_DRAGON_JSON = "{\"name\":\"Ancient Black Dragon\",\"size\":\"gargantuan\",\"type\":\"Dragon\",\"tag\":\"\",\"alignment\":\"chaotic evil\",\"hitDice\":\"21\",\"armorName\":\"natural armor\",\"shieldBonus\":0,\"natArmorBonus\":10,\"otherArmorDesc\":\"22 (natural armor)\",\"speed\":\"40\",\"burrowSpeed\":\"0\",\"climbSpeed\":\"0\",\"flySpeed\":\"80\",\"hover\":false,\"swimSpeed\":\"40\",\"customHP\":false,\"customSpeed\":false,\"hpText\":\"367 (21d20 + 147)\",\"speedDesc\":\"40 ft., fly 80 ft., swim 40 ft.\",\"strPoints\":\"27\",\"dexPoints\":\"14\",\"conPoints\":\"25\",\"intPoints\":\"16\",\"wisPoints\":\"15\",\"chaPoints\":\"19\",\"blindsight\":\"60\",\"blind\":false,\"darkvision\":\"120\",\"tremorsense\":\"0\",\"truesight\":\"0\",\"telepathy\":0,\"cr\":\"21\",\"customCr\":\"21 (33,000 XP)\",\"customProf\":7,\"isLegendary\":true,\"legendariesDescription\":\"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.\",\"isLair\":true,\"lairDescription\":\"When fighting inside its lair, the ancient black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the ancient black dragon can take one lair action to cause one of the following effects:\",\"lairDescriptionEnd\":\"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.\",\"isMythic\":true,\"mythicDescription\":\"If the ancient black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.\",\"isRegional\":true,\"regionalDescription\":\"The region containing the ancient black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:\",\"regionalDescriptionEnd\":\"If the ancient black dragon dies, the first two effects fade over the course of 3d10 days.\",\"properties\":[],\"abilities\":[{\"name\":\"Amphibious\",\"desc\":\"The dragon can breathe air and water.\"},{\"name\":\"Legendary Resistance (3/Day)\",\"desc\":\"If the dragon fails a saving throw, it can choose to succeed instead.\"}],\"actions\":[{\"name\":\"Multiattack\",\"desc\":\"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.\"},{\"name\":\"Bite\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 15 ft., one target. _Hit:_ 19 (2d10 + 8) piercing damage plus 9 (2d8) acid damage.\"},{\"name\":\"Claw\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 10 ft., one target. _Hit:_ 15 (2d6 + 8) slashing damage.\"},{\"name\":\"Tail\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 20 ft., one target. _Hit:_ 17 (2d8 + 8) bludgeoning damage.\"},{\"name\":\"Frightful Presence\",\"desc\":\"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours.\"},{\"name\":\"Acid Breath (Recharge 5-6)\",\"desc\":\"The dragon exhales acid in a 90-foot line that is 10 feet wide. Each creature in that line must make a DC 22 Dexterity saving throw, taking 67 (15d8) acid damage on a failed save, or half as much damage on a successful one.\"}],\"bonusActions\":[],\"reactions\":[],\"legendaries\":[{\"name\":\"Detect\",\"desc\":\"The dragon makes a Wisdom (Perception) check.\"},{\"name\":\"Tail Attack\",\"desc\":\"The dragon makes a tail attack.\"},{\"name\":\"Wing Attack (Costs 2 Actions)\",\"desc\":\"The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 23 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.\"}],\"mythics\":[],\"lairs\":[],\"regionals\":[],\"sthrows\":[{\"name\":\"dex\",\"order\":1},{\"name\":\"con\",\"order\":2},{\"name\":\"wis\",\"order\":4},{\"name\":\"cha\",\"order\":5}],\"skills\":[{\"name\":\"perception\",\"stat\":\"wis\",\"note\":\" (ex)\"},{\"name\":\"stealth\",\"stat\":\"dex\"}],\"damagetypes\":[{\"name\":\"acid\",\"note\":\" (Immune)\",\"type\":\"i\"}],\"specialdamage\":[],\"conditions\":[],\"languages\":[{\"name\":\"Common\",\"speaks\":true},{\"name\":\"Draconic\",\"speaks\":true}],\"understandsBut\":\"\",\"shortName\":\"\",\"pluralName\":\"\",\"doubleColumns\":false,\"separationPoint\":5,\"damage\":[]}";
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void canImport_validTetraCubeJson_returnsTrue() {
|
|
||||||
assertTrue(importer.canImport(SAMPLE_TETRACUBE_JSON));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void canImport_invalidJson_returnsFalse() {
|
|
||||||
assertFalse(importer.canImport("not a json string"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void canImport_emptyString_returnsFalse() {
|
|
||||||
assertFalse(importer.canImport(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void canImport_otherJson_returnsFalse() {
|
|
||||||
assertFalse(importer.canImport("{\"foo\": \"bar\"}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_validTetraCubeJson_createsMonster() throws Exception {
|
|
||||||
Monster monster = importer.parse(SAMPLE_TETRACUBE_JSON);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals("Goblin", monster.name);
|
|
||||||
assertEquals("Small", monster.size);
|
|
||||||
assertEquals("humanoid", monster.type);
|
|
||||||
assertEquals("goblinoid", monster.subtype);
|
|
||||||
assertEquals("neutral evil", monster.alignment);
|
|
||||||
assertEquals(2, monster.hitDice);
|
|
||||||
assertEquals(30, monster.walkSpeed);
|
|
||||||
assertEquals(8, monster.strengthScore);
|
|
||||||
assertEquals(14, monster.dexterityScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_bonusActions_populatesBonusActions() throws Exception {
|
|
||||||
String json = "{"
|
|
||||||
+ "\"name\": \"Goblin Boss\","
|
|
||||||
+ "\"hitDice\": 4,"
|
|
||||||
+ "\"bonusActions\": [{\"name\": \"Nimble Escape\", \"desc\": \"Disengage or Hide as bonus action.\"}]"
|
|
||||||
+ "}";
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals(1, monster.bonusActions.size());
|
|
||||||
assertEquals("Nimble Escape", monster.bonusActions.get(0).name);
|
|
||||||
assertEquals("Disengage or Hide as bonus action.", monster.bonusActions.get(0).description);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_blindsightWithBlindFlag_appendsNote() throws Exception {
|
|
||||||
String json = "{"
|
|
||||||
+ "\"name\": \"Grimlock\","
|
|
||||||
+ "\"hitDice\": 3,"
|
|
||||||
+ "\"blindsight\": 30,"
|
|
||||||
+ "\"blind\": true"
|
|
||||||
+ "}";
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertTrue(monster.senses.contains("blindsight 30 ft. (blind beyond this radius)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_legendaryAndMythicDescriptions_parsedCorrectly() throws Exception {
|
|
||||||
String json = "{"
|
|
||||||
+ "\"name\": \"Ancient Dragon\","
|
|
||||||
+ "\"hitDice\": 20,"
|
|
||||||
+ "\"legendariesDescription\": \"The dragon can take 3 legendary actions.\","
|
|
||||||
+ "\"mythicDescription\": \"If mythic trait is active...\","
|
|
||||||
+ "\"mythics\": [{\"name\": \"Grasp\", \"desc\": \"Grabs a creature.\"}]"
|
|
||||||
+ "}";
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals(3, monster.legendaryActions.size());
|
|
||||||
assertEquals("Legendary Actions", monster.legendaryActions.get(0).name);
|
|
||||||
assertEquals("Mythic Actions", monster.legendaryActions.get(1).name);
|
|
||||||
assertEquals("Mythic Action: Grasp", monster.legendaryActions.get(2).name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_lairAndRegionalDescriptions_parsedCorrectly() throws Exception {
|
|
||||||
String json = "{"
|
|
||||||
+ "\"name\": \"Lich\","
|
|
||||||
+ "\"hitDice\": 18,"
|
|
||||||
+ "\"lairDescription\": \"Lair intro.\","
|
|
||||||
+ "\"lairDescriptionEnd\": \"Lair end.\","
|
|
||||||
+ "\"regionalDescription\": \"Regional intro.\","
|
|
||||||
+ "\"regionalDescriptionEnd\": \"Regional end.\""
|
|
||||||
+ "}";
|
|
||||||
Monster monster = importer.parse(json);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals(1, monster.lairActions.size());
|
|
||||||
assertEquals("Lair Actions", monster.lairActions.get(0).name);
|
|
||||||
assertEquals("Lair intro.\n\nLair end.", monster.lairActions.get(0).description);
|
|
||||||
assertEquals(1, monster.regionalActions.size());
|
|
||||||
assertEquals("Regional Effects", monster.regionalActions.get(0).name);
|
|
||||||
assertEquals("Regional intro.\n\nRegional end.", monster.regionalActions.get(0).description);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void parse_ancientBlackDragonExport_populatesAllSections() throws Exception {
|
|
||||||
Monster monster = importer.parse(ANCIENT_BLACK_DRAGON_JSON);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals("Ancient Black Dragon", monster.name);
|
|
||||||
assertEquals("gargantuan", monster.size);
|
|
||||||
assertEquals("Dragon", monster.type);
|
|
||||||
assertEquals("chaotic evil", monster.alignment);
|
|
||||||
assertEquals(27, monster.strengthScore);
|
|
||||||
|
|
||||||
// Abilities
|
|
||||||
assertEquals(2, monster.abilities.size());
|
|
||||||
assertEquals("Amphibious", monster.abilities.get(0).name);
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
assertEquals(6, monster.actions.size());
|
|
||||||
assertEquals("Multiattack", monster.actions.get(0).name);
|
|
||||||
|
|
||||||
// Legendary + Mythic Actions
|
|
||||||
// 1 legendary intro + 3 legendary actions + 1 mythic intro = 5 items
|
|
||||||
assertEquals(5, monster.legendaryActions.size());
|
|
||||||
assertEquals("Legendary Actions", monster.legendaryActions.get(0).name);
|
|
||||||
assertEquals("Detect", monster.legendaryActions.get(1).name);
|
|
||||||
assertEquals("Mythic Actions", monster.legendaryActions.get(4).name);
|
|
||||||
|
|
||||||
// Lair Actions
|
|
||||||
assertEquals(1, monster.lairActions.size());
|
|
||||||
assertEquals("Lair Actions", monster.lairActions.get(0).name);
|
|
||||||
assertTrue(monster.lairActions.get(0).description.contains("invoke the ambient magic"));
|
|
||||||
|
|
||||||
// Regional Effects
|
|
||||||
assertEquals(1, monster.regionalActions.size());
|
|
||||||
assertEquals("Regional Effects", monster.regionalActions.get(0).name);
|
|
||||||
assertTrue(monster.regionalActions.get(0).description.contains("warped by the creature's presence"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void monsterImportHelper_fromJSON_delegatesToImporter() {
|
|
||||||
Monster monster = MonsterImportHelper.fromJSON(SAMPLE_TETRACUBE_JSON);
|
|
||||||
assertNotNull(monster);
|
|
||||||
assertEquals("Goblin", monster.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,16 +5,29 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:9.3.2'
|
classpath "com.android.tools.build:gradle:7.0.1"
|
||||||
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
|
||||||
def nav_version = "2.9.8"
|
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
// maven { url "https://oss.jfrog.org/libs-snapshot" }
|
||||||
|
// maven { url 'http://repo1.maven.org/maven2' }
|
||||||
|
}
|
||||||
|
// Temporary workaround until room 2.4.0 is released https://issuetracker.google.com/issues/174695268?pli=1#comment10
|
||||||
|
configurations.configureEach {
|
||||||
|
resolutionStrategy {
|
||||||
|
force 'org.xerial:sqlite-jdbc:3.34.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,14 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. For more details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
# Android operating system, and which are packaged with your app"s APK
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
android.enableJetifier=false
|
android.enableJetifier=true
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
android.uniquePackageNames=false
|
|
||||||
android.dependency.useConstraints=false
|
|
||||||
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
|
||||||
android.r8.strictFullModeForKeepRules=false
|
|
||||||
org.gradle.configuration-cache=true
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#This file is generated by updateDaemonJvm
|
|
||||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
|
||||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
|
||||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
|
||||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
|
||||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
|
||||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
|
||||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
|
||||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
|
||||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
|
||||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
|
||||||
toolchainVendor=JETBRAINS
|
|
||||||
toolchainVersion=21
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#Fri Aug 14 17:08:57 PDT 2026
|
#Sat May 01 23:00:30 PDT 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||||
|
|||||||
@@ -1,26 +1,2 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google {
|
|
||||||
content {
|
|
||||||
includeGroupByRegex("com\\.android.*")
|
|
||||||
includeGroupByRegex("com\\.google.*")
|
|
||||||
includeGroupByRegex("androidx.*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
plugins {
|
|
||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
|
||||||
}
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "MonsterCards"
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
rootProject.name = "MonsterCards"
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
# Import & Export Architecture & Plan
|
|
||||||
|
|
||||||
This document outlines the modular architecture and step-by-step technical roadmap for handling monster stat block imports, exports, and sharing across multiple formats in the **Monster Cards** application.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. File Formats & Importer Architecture
|
|
||||||
|
|
||||||
Monster Cards separates canonical native formats from third-party import sources:
|
|
||||||
|
|
||||||
1. **Universal Native Format (`.card` / `.card.txt`)**:
|
|
||||||
- Our native file format utilizing the **Universal 2-Tier Envelope Specification** (`$schema`, `uuid`, `ruleset_id`, `entity_type`, `display_name`, `template_id`, `properties`).
|
|
||||||
- Uses the **Open5e Ruleset** (`ruleset_id: "open5e"`) which matches our app's 5e monster and character needs ([`entities/character.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/entities/character.json)).
|
|
||||||
- Supported for both **Export** and **Native Import**.
|
|
||||||
2. **Third-Party Import-Only Formats**:
|
|
||||||
- `.monster` / `.monster.txt`: Tetra-cube generator stat block JSON format (Import-only).
|
|
||||||
- D&D Beyond URLs / API payloads: D&D Beyond character web service format (Import-only).
|
|
||||||
|
|
||||||
All format parsers implement a shared interface:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface EntityImporter<T> {
|
|
||||||
/**
|
|
||||||
* Determines whether the given input string or payload can be handled by this importer.
|
|
||||||
*/
|
|
||||||
boolean canImport(@NonNull String input);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the raw input string into internal domain memory objects (e.g., Monster).
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
T parse(@NonNull String input) throws Exception;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The output of any `EntityImporter` is an in-memory domain model (such as `Monster`), which is then passed to `MonsterImportFragment` for UI preview and Room database persistence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Step-by-Step Implementation Roadmap
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 0: Restrict File Extension Intent Filters │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 1: Refactor Import Code to Shared Interface │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 2: Update Tetra-cube Importer to Newest Format │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 3: Import from D&D Beyond Character URL │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 4: Export to Custom Internal Format (Open5e) │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 5: Import from Custom Internal Format (Open5e) │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 6: Generic Android Share Button Feature │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────----------------────────────────────────┐
|
|
||||||
│ Step 7: Specific Share Targets (NFC, Bluetooth, URL) │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 0: File Extension Intent Filters (.monster & .card)
|
|
||||||
|
|
||||||
Restrict the Android app so it reacts to supported stat block file extensions (`.monster`, `.monster.txt` for Tetra-cube format; `.card`, `.card.txt` for internal Open5e card format) instead of all generic `.txt` files.
|
|
||||||
|
|
||||||
1. **`AndroidManifest.xml`**:
|
|
||||||
Add `android:pathPattern` constraints (`.monster`, `.monster.txt`, `.card`, `.card.txt`) to the `<intent-filter>` for `MainActivity`.
|
|
||||||
2. **`MainActivity.java`**:
|
|
||||||
Implement runtime display name validation querying `OpenableColumns.DISPLAY_NAME` via `ContentResolver` to filter out non-monster files passed via `content://` URIs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 1: Refactor Import Code into Importer Architecture
|
|
||||||
|
|
||||||
Extract the current parsing logic out of `MonsterImportHelper.java` into a standalone importer class implementing `EntityImporter<Monster>`.
|
|
||||||
|
|
||||||
- **New Class**: `TetraCubeMonsterImporter implements EntityImporter<Monster>`
|
|
||||||
- **Responsibilities**:
|
|
||||||
- `canImport(json)`: Validates that the JSON string contains Tetra-cube properties (e.g. `hitDice`, `armorName`, `strPoints`).
|
|
||||||
- `parse(json)`: Converts the raw JSON payload into an in-memory `Monster` object.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Update Tetra-cube Importer to Newest Format
|
|
||||||
|
|
||||||
Update `TetraCubeMonsterImporter` to support newer fields from the Tetra-cube generator source (`js/statblock-script.js`):
|
|
||||||
|
|
||||||
- **New Fields to Parse**:
|
|
||||||
- `bonusActions`: Array of `{ name, desc }` objects $\rightarrow$ mapped to `monster.actions` or bonus action structure.
|
|
||||||
- `mythics` / `isMythic` / `mythicDescription`: Array of mythic action objects and intro text $\rightarrow$ mapped to `monster.legendaryActions`.
|
|
||||||
- `blind`: Parse boolean flag to append `"(blind beyond this radius)"` to `blindsight`.
|
|
||||||
- Section intro text: Parse `legendariesDescription`, `lairDescription`, `regionalDescription`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Import from D&D Beyond URL & Web Services
|
|
||||||
|
|
||||||
Allow users to import characters, monsters, and future entity types directly from D&D Beyond URLs using a multi-channel ingestion architecture.
|
|
||||||
|
|
||||||
- **Primary Target Share Links**:
|
|
||||||
- Site Share Link: `https://www.dndbeyond.com/characters/49074997/s7sLyX` (primary link copied when tapping "Share" on D&D Beyond; contains numeric character ID `49074997` and share token `s7sLyX`).
|
|
||||||
- Standard Web Link: `https://www.dndbeyond.com/characters/49074997` (HTML page that loads background JavaScript to fetch stats).
|
|
||||||
- URL Extractor Regex: `https?://(?:www\.)?dndbeyond\.com/characters/(\d+)(?:/([a-zA-Z0-9]+))?`
|
|
||||||
|
|
||||||
- **URL Ingestion Channels**:
|
|
||||||
1. **Android Share Sheet (`ACTION_SEND`)**:
|
|
||||||
- Register `intent-filter` for `ACTION_SEND` with `text/plain` in `AndroidManifest.xml`.
|
|
||||||
- Tapping "Share" on D&D Beyond passes `https://www.dndbeyond.com/characters/49074997/s7sLyX` directly into Monster Cards.
|
|
||||||
2. **Android Web Link Chooser (`ACTION_VIEW`)**:
|
|
||||||
- Register `intent-filter` for `ACTION_VIEW` in `AndroidManifest.xml` with scheme `https`, host `www.dndbeyond.com`, and path prefixes `/characters/`, `/monsters/`, `/spells/`, etc.
|
|
||||||
- Allows Monster Cards to appear in the Android "Open with..." link chooser dialog.
|
|
||||||
3. **In-App Menu & Clipboard Auto-Detection**:
|
|
||||||
- Add an "Import from URL..." menu item in `LibraryFragment` and `MonsterImportFragment`.
|
|
||||||
- Displays a URL input dialog with automatic clipboard URL detection (`ClipboardManager`).
|
|
||||||
|
|
||||||
- **Known Endpoints & Web Asset Structure**:
|
|
||||||
- **Main Character Service Endpoint**:
|
|
||||||
- `https://character-service.dndbeyond.com/character/v5/character/49074997?includeCustomItems=true`
|
|
||||||
- Returns JSON payload containing full character statistics, attributes, modifiers, classes, race, inventory, and spells.
|
|
||||||
- Full reference sample JSON payload extracted to: [`docs/dndbeyond-character-sample.json`](file:///Users/tom/Projects/Apps/MonsterCards/docs/dndbeyond-character-sample.json).
|
|
||||||
- **Auxiliary Service Endpoints**:
|
|
||||||
- Vehicles: `https://character-service.dndbeyond.com/character/v5/vehicles?characterId=49074997`
|
|
||||||
- Vehicle Components: `https://character-service.dndbeyond.com/character/v5/vehicle/components?characterId=49074997`
|
|
||||||
- Known Infusions: `https://character-service.dndbeyond.com/character/v5/known-infusions?characterId=49074997`
|
|
||||||
- Infusion Items: `https://character-service.dndbeyond.com/character/v5/infusion/items?characterId=49074997`
|
|
||||||
- **Image & Avatar Assets**:
|
|
||||||
- Default Builder Avatar: `https://www.dndbeyond.com/Content/Skins/Waterdeep/images/characters/default-avatar-builder.png`
|
|
||||||
- Race Portrait Avatar: `portraitAvatarUrl` (e.g., `https://www.dndbeyond.com/avatars/2489/881/636680412207671648.jpeg`)
|
|
||||||
- Item / Attunement Icons: Returned in `inventory[].definition.avatarUrl` (e.g. `https://www.dndbeyond.com/avatars/19/144/636382339478303209.jpeg`, `https://www.dndbeyond.com/avatars/9249/564/637203446409923453.jpeg`).
|
|
||||||
- **Analytics / Telemetry**:
|
|
||||||
- `https://global.ketchcdn.com/web/v2/log...` (returns 204 No Content; user consent log endpoint ignored by importer).
|
|
||||||
|
|
||||||
- **Architecture (`DnDBeyondImporter implements EntityImporter<Monster>`)**:
|
|
||||||
- **URL Extractor**: Extracts `{characterId}` (`49074997`) from shared URLs (`https://www.dndbeyond.com/characters/49074997/s7sLyX`), trimming optional share tokens.
|
|
||||||
- **HTTP Service Client**: Performs an asynchronous HTTP GET request (via OkHttp/Retrofit) to `character-service.dndbeyond.com/character/v5/character/{characterId}?includeCustomItems=true`.
|
|
||||||
- **Domain Mapper**: Maps D&D Beyond JSON fields (stats, modifiers, AC, HP, speed, proficiencies, actions, bonus actions, reactions, spells, traits, avatar URLs) into the internal `Monster` domain model (`sourceUrl = "https://www.dndbeyond.com/characters/49074997/s7sLyX"`).
|
|
||||||
- **UI Preview**: Passes the generated `Monster` object to `MonsterImportFragment` for review and persistence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4: Export to Custom Internal Format (Open5e Specification)
|
|
||||||
|
|
||||||
Implement export capability to output monsters and collections using our two-tier Open5e JSON specification (detailed in [`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md)).
|
|
||||||
|
|
||||||
- **New Class**: `Open5eExporter`
|
|
||||||
|
|
||||||
#### Open5e Ruleset Architecture & Schema Specification
|
|
||||||
The Open5e ruleset is part of a modular, versioned ruleset framework located at [`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/). In the future, this ruleset will be embedded directly into the application assets and versioned as our canonical model reference.
|
|
||||||
|
|
||||||
1. **Ruleset & Entity Identifiers**:
|
|
||||||
- Every ruleset defines a short string ID (`ruleset_id: "open5e"`) and a unique UUID (`uuid: "7a35e4d2-f67b-4890-a292-6a7593c72b21"`), declared in the ruleset manifest [`manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/manifest.json).
|
|
||||||
- Exported entities contain top-level identifiers (`uuid`, `ruleset_id`, `entity_type`, `display_name`) wrapped in a universal envelope.
|
|
||||||
- When importing a JSON payload like [`examples/character/goblin.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/examples/character/goblin.json), importers inspect `ruleset_id == "open5e"` to match the registered ruleset parser.
|
|
||||||
|
|
||||||
2. **Base Schemas ([`/Users/tom/Projects/TTRPG/CharacterDataFiles/schema/*.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/))**:
|
|
||||||
- **[`schema/entity.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/entity.json)**: The universal envelope schema wrapping all exported entities (`uuid`, `ruleset_id`, `entity_type`, `display_name`, `description`, `version`, `tags`, `template_id`, `properties`).
|
|
||||||
- **[`schema/manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/manifest.json)**: Schema defining ruleset manifest metadata, registered entity types, and sheet templates.
|
|
||||||
- **[`schema/entity-definition.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/entity-definition.json)**: Meta-schema describing how entity types and property schemas are declared.
|
|
||||||
|
|
||||||
3. **Character Entity Schema ([`entities/character.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/entities/character.json))**:
|
|
||||||
- Defines the `properties` payload for Player Characters (PCs), NPCs, and Monsters/Creatures.
|
|
||||||
- Core fields in `properties`:
|
|
||||||
- **Identity & Vitals**: `size`, `type`, `subtype`, `alignment`, `armor_class`, `armor_description`, `hit_points` (`current`, `max`, `formula`, `hit_dice`), `speed`, `speed_desc`, `challenge_rating`, `cr`.
|
|
||||||
- **Ability Scores**: `abilities` (`strength`, `dexterity`, `constitution`, `intelligence`, `wisdom`, `charisma`).
|
|
||||||
- **Proficiencies & Senses**: `saving_throws`, `skills`, `senses`, `languages`.
|
|
||||||
- **Actions & Traits**: `traits`, `actions`, `reactions`, `legendary_actions`, `lair_actions`, `regional_effects`, and `spellcasting`.
|
|
||||||
|
|
||||||
4. **Rendering Templates (`template_id`)**:
|
|
||||||
- The `template_id` field in the universal envelope specifies how the entity should be rendered in external or host applications.
|
|
||||||
- Registered templates in [`manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/manifest.json):
|
|
||||||
- `"stat_block"`: Monster Manual style stat block (`templates/stat_block.html`).
|
|
||||||
- `"character_card"`: Compact NPC summary card (`templates/character_card.html`).
|
|
||||||
- `"character_list_item"`: Fixed-height quick-reference list item (`templates/character_list_item.html`).
|
|
||||||
- `"character_sheet"` / `"character_sheet_alt"`: Full 5e interactive character sheets.
|
|
||||||
|
|
||||||
#### Exporter Output Requirements (`Open5eExporter`)
|
|
||||||
- **Envelope Generation**:
|
|
||||||
- Emits `$schema: "../../../schema/entity.json"`.
|
|
||||||
- Populates `uuid` (random UUID v4), `ruleset_id: "open5e"`, `entity_type: "character"`, `display_name: monster.name`, `template_id: "stat_block"`.
|
|
||||||
- **Payload Mapping**:
|
|
||||||
- Maps `Monster` fields to Open5e character properties in `properties` object conforming to [`entities/character.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/entities/character.json).
|
|
||||||
|
|
||||||
#### Open5e vs. Monster Cards Database Schema Comparison & Final Mappings
|
|
||||||
- **Database Storage Architecture (Room SQLite vs. Open5e JSON)**:
|
|
||||||
- **Ability Scores**: Kept as flat integer columns in Room (`strength_score`, `dexterity_score`, etc.) for maximum query performance. When exporting to Open5e JSON, `Open5eExporter` packages these into a `properties.abilities` JSON object `{ "strength": 12, ... }`. Modifiers are calculated dynamically using `floor((score - 10) / 2)`.
|
|
||||||
- **Variable-Length Lists (`traits`, `actions`, `skills`, `languages`)**: Stored as text columns using Room `@TypeConverter` JSON converters. In Open5e JSON export, traits and actions are formatted as `{ "name": "...", "desc": "..." }` arrays.
|
|
||||||
- **PC Identity Fields**: Added PC identity columns (`player_name`, `background`, `personality_traits`, `ideals`, `bonds`, `flaws`, `age`, `height`, `weight`, `eyes`, `skin`, `hair`, `appearance`, `backstory`, `allies_and_organizations`) directly to `Monster.java` (`Database version 7`, `MIGRATION_6_7`).
|
|
||||||
- **Omitted PC Fields**: `currency`, `inventory`, `death_saves`, and `spellcasting.slots` are ignored/omitted in monster stat block exports.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5: Import from Custom Internal Format (Open5e Specification)
|
|
||||||
|
|
||||||
Implement import capability for custom Open5e JSON files.
|
|
||||||
|
|
||||||
- **New Class**: `Open5eImporter implements EntityImporter<Monster>`
|
|
||||||
- **Pipeline**:
|
|
||||||
- `canImport(json)`: Validates `"ruleset_id": "open5e"` and `"entity_type": "character"`.
|
|
||||||
- `parse(json)`: Unpacks `properties` into internal `Monster` domain models.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 6: Generic Android Share Button Feature
|
|
||||||
|
|
||||||
Implement a generic "Share" feature allowing users to export and share monsters or collections from detail screens.
|
|
||||||
|
|
||||||
- **UI Action**: Add a "Share" item to action menus in `MonsterDetailFragment` and `CollectionDetailFragment`.
|
|
||||||
- **Android Intent**: Uses standard Android `ACTION_SEND` intent with `Intent.EXTRA_STREAM` or `Intent.EXTRA_TEXT` to pass exported files to other apps (e.g. Email, Drive, Messaging, Files).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 7: Specific Share Targets & Channels
|
|
||||||
|
|
||||||
Extend the generic sharing feature with specialized, direct sharing channels (to be implemented as individual sub-steps):
|
|
||||||
|
|
||||||
- **Sub-Step 7.1 (NFC Sharing)**: Share monster data directly between devices via NFC (NDEF records / Android Beam).
|
|
||||||
- **Sub-Step 7.2 (Bluetooth / Wi-Fi Direct)**: Share monster files directly between nearby Android devices via Bluetooth / Wi-Fi.
|
|
||||||
- **Sub-Step 7.3 (Web URL with Embedded Payload)**: Generate a shareable Web URL containing compressed/base64-encoded monster JSON data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Implementation Checklist & Status
|
|
||||||
|
|
||||||
- [x] **Step 0**: Restrict app intent filters in `AndroidManifest.xml` (`.monster` & `.monster.txt`) and add runtime filename validation in `MainActivity.java`.
|
|
||||||
- [x] **Step 1**: Refactor import & conversion code into a shared `EntityImporter<T>` interface and `TetraCubeMonsterImporter` class.
|
|
||||||
- [x] **Step 2**: Update Tetra-cube importer class to support the newest Tetra-cube format (`bonusActions`, `mythics`, `blind`, intro descriptions).
|
|
||||||
- [x] **Step 3**: Import from D&D Beyond URL (`https://www.dndbeyond.com/characters/49074997` fetching from v5 character service endpoint).
|
|
||||||
- [x] **Step 4**: Export to internal format described by Open5e document (`Open5eExporter`).
|
|
||||||
- [x] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
|
|
||||||
- [x] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
|
|
||||||
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Schema Planning & Text Formatting Notes
|
|
||||||
|
|
||||||
### 4.1 Schema Architecture Considerations
|
|
||||||
- **Unified Action Entity / Type Column**:
|
|
||||||
- Instead of maintaining separate `List<Trait>` columns for each category (`actions`, `reactions`, `legendaryActions`, `lairActions`, `regionalActions`, etc.), a unified `monster_actions` table (or model list) with an `action_type` column (`ABILITY`, `ACTION`, `BONUS_ACTION`, `REACTION`, `LEGENDARY_ACTION`, `MYTHIC_ACTION`, `LAIR_ACTION`, `REGIONAL_EFFECT`) simplifies Room DB queries and enables a single reusable editor UI component.
|
|
||||||
- **Section Intro & End Note Metadata**:
|
|
||||||
- Store section-level intro text and end notes (e.g. `legendaryActionsDescription`, `lairActionsDescription`, `lairActionsEndNote`, `regionalActionsDescription`, `regionalActionsEndNote`, `mythicActionsDescription`) as dedicated metadata fields on the `Monster` entity rather than as artificial action traits.
|
|
||||||
|
|
||||||
### 4.2 Markdown & Content Formatting Support
|
|
||||||
- **Supported Markdown Syntax**:
|
|
||||||
- **Emphasis**: Italics (`_text_` or `*text*`) and Bold (`__text__` or `**text**`).
|
|
||||||
- **Lists**: Bulleted lists (`- `, `* `) and Numbered lists (`1. `, `2. `).
|
|
||||||
- **External Links**: Standard markdown links `[Text](https://...)`.
|
|
||||||
- **Internal Links (Future Deep Linking)**: Custom URI scheme `[Label](mc://<type>/<id>)` (e.g., `[Fireball](mc://spell/fireball)`) reserved for in-app navigation.
|
|
||||||
- **Data Model & Import/Export Handling**:
|
|
||||||
- All text fields across domain models, Room persistence, and import/export payloads store raw CommonMark markdown strings.
|
|
||||||
- Importers preserve raw markdown syntax as exported by source formats (Tetra-cube, Open5e, D&D Beyond).
|
|
||||||
- UI rendering layers handle conversion from CommonMark to Android Spanned text at display time.
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
# Import Process & Schema Documentation
|
|
||||||
|
|
||||||
> **Platform Note**: This documentation describes the import process and internal database schema for the **Android Application** (`com.majinnaibu.monstercards`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Overview of the Import Process
|
|
||||||
|
|
||||||
The Monster Cards Android application can import monster stat blocks formatted in JSON from `.monster` or `.monster.txt` files shared or opened from other applications (such as file managers, messaging apps, or browsers).
|
|
||||||
|
|
||||||
```
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
| External .monster / .monster.txt file (JSON Payload) |
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
| MainActivity (Intent Handling: ACTION_VIEW / SEND) |
|
|
||||||
| - Extracts raw JSON string from Content/File URI |
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
| MonsterImportFragment & MonsterImportViewModel |
|
|
||||||
| - MonsterImportHelper.fromJSON(json) parses JSON |
|
|
||||||
| - Maps JSON fields to internal Monster domain model |
|
|
||||||
| - Renders UI preview with calculated AC, HP, & Markdown|
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
| User clicks "Import" Menu Action |
|
|
||||||
| - Generates new UUID |
|
|
||||||
| - Persists Monster to Room Database (monsters table) |
|
|
||||||
| - Rebuilds FTS index (monsters_fts) |
|
|
||||||
+-------------------------------------------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Source Format (.monster / .monster.txt) & File Extension Behavior
|
|
||||||
|
|
||||||
The `.monster` file format is generated by Tetra-cube's D&D 5e Statblock Generator (`statblock-script.js`).
|
|
||||||
|
|
||||||
### File Saving Behavior (`SavedData.SaveToFile`)
|
|
||||||
```javascript
|
|
||||||
SaveToFile: () => saveAs(new Blob([JSON.stringify(mon)], {
|
|
||||||
type: "text/plain;charset=utf-8"
|
|
||||||
}), mon.name.toLowerCase() + ".monster")
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Desktop Browsers**: Save the file with the specified `.monster` filename.
|
|
||||||
- **Android Mobile Browsers & DownloadManager**: Because the `Blob` uses MIME type `text/plain;charset=utf-8`, Android file handlers append a `.txt` extension, creating `.monster.txt` files.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. JSON Parsing & Field Mapping (`MonsterImportHelper.java`)
|
|
||||||
|
|
||||||
`MonsterImportHelper.fromJSON(String json)` uses Gson's `JsonParser` to parse the root JSON object and populate a `Monster` instance.
|
|
||||||
|
|
||||||
### Complete Field Mapping Reference
|
|
||||||
|
|
||||||
| JSON Field Name | Target `Monster` Field | Type / Default | Description / Conversion Logic |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| `name` | `monster.name` | `String` | Monster name |
|
|
||||||
| `size` | `monster.size` | `String` | Size string (e.g., `"medium"`, `"large"`) |
|
|
||||||
| `type` | `monster.type` | `String` | Creature type (e.g., `"humanoid"`, `"dragon"`) |
|
|
||||||
| `tag` | `monster.subtype` | `String` | Subtype / tag (e.g., `"elf"`, `"shapechanger"`) |
|
|
||||||
| `alignment` | `monster.alignment` | `String` | Alignment string (e.g., `"neutral good"`) |
|
|
||||||
| `hitDice` | `monster.hitDice` | `int` (default 1) | Number of hit dice |
|
|
||||||
| `armorName` | `monster.armorType` | `ArmorType` | Converted via `ArmorTypeConverter.armorTypeFromStringValue()` |
|
|
||||||
| `shieldBonus` | `monster.shieldBonus` | `int` | Shield AC bonus (+2 if present) |
|
|
||||||
| `natArmorBonus` | `monster.naturalArmorBonus` | `int` | Additional natural armor AC bonus |
|
|
||||||
| `otherArmorDesc` | `monster.otherArmorDescription` | `String` | Custom armor description string |
|
|
||||||
| `speed` | `monster.walkSpeed` | `int` | Walking speed in feet |
|
|
||||||
| `burrowSpeed` | `monster.burrowSpeed` | `int` | Burrowing speed in feet |
|
|
||||||
| `climbSpeed` | `monster.climbSpeed` | `int` | Climbing speed in feet |
|
|
||||||
| `flySpeed` | `monster.flySpeed` | `int` | Flying speed in feet |
|
|
||||||
| `hover` | `monster.canHover` | `boolean` | Whether flying creature can hover |
|
|
||||||
| `swimSpeed` | `monster.swimSpeed` | `int` | Swimming speed in feet |
|
|
||||||
| `customHP` | `monster.hasCustomHP` | `boolean` | Flag for override custom HP string |
|
|
||||||
| `customSpeed` | `monster.hasCustomSpeed` | `boolean` | Flag for override custom speed string |
|
|
||||||
| `hpText` | `monster.customHPDescription` | `String` | Custom HP text description |
|
|
||||||
| `speedDesc` | `monster.customSpeedDescription` | `String` | Custom speed text description |
|
|
||||||
| `strPoints` | `monster.strengthScore` | `int` (default 10) | Strength ability score |
|
|
||||||
| `dexPoints` | `monster.dexterityScore` | `int` (default 10) | Dexterity ability score |
|
|
||||||
| `conPoints` | `monster.constitutionScore` | `int` (default 10) | Constitution ability score |
|
|
||||||
| `intPoints` | `monster.intelligenceScore` | `int` (default 10) | Intelligence ability score |
|
|
||||||
| `wisPoints` | `monster.wisdomScore` | `int` (default 10) | Wisdom ability score |
|
|
||||||
| `chaPoints` | `monster.charismaScore` | `int` (default 10) | Charisma ability score |
|
|
||||||
| `blindsight` | `monster.senses` | `Set<String>` | Added as `"blindsight <N> ft."` if `N > 0` |
|
|
||||||
| `darkvision` | `monster.senses` | `Set<String>` | Added as `"darkvision <N> ft."` if `N > 0` |
|
|
||||||
| `tremorsense` | `monster.senses` | `Set<String>` | Added as `"tremorsense <N> ft."` if `N > 0` |
|
|
||||||
| `truesight` | `monster.senses` | `Set<String>` | Added as `"truesight <N> ft."` if `N > 0` |
|
|
||||||
| `telepathy` | `monster.telepathyRange` | `int` | Telepathy range in feet |
|
|
||||||
| `cr` | `monster.challengeRating` | `ChallengeRating` | Converted via `ChallengeRatingConverter` |
|
|
||||||
| `customCr` | `monster.customChallengeRatingDescription` | `String` | Custom CR text |
|
|
||||||
| `customProf` | `monster.customProficiencyBonus` | `int` | Custom proficiency bonus override |
|
|
||||||
| `abilities` | `monster.abilities` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `actions` | `monster.actions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `bonusActions` | `monster.actions` *(Future)* | `List<Trait>` | Bonus actions array |
|
|
||||||
| `reactions` | `monster.reactions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `legendaries` | `monster.legendaryActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `mythics` | `monster.legendaryActions` *(Future)* | `List<Trait>` | Mythic actions array |
|
|
||||||
| `lairs` | `monster.lairActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `regionals` | `monster.regionalActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
|
|
||||||
| `sthrows` | Saving throw proficiencies | `ProficiencyType` | Array of objects `{ "name": "str"\|"dex"\|"con"\|"int"\|"wis"\|"cha" }`. Sets proficiency to `PROFICIENT`. |
|
|
||||||
| `skills` | `monster.skills` | `Set<Skill>` | Array of `{ "name": "...", "stat": "...", "note": " (ex)" }`. `note == " (ex)"` sets `EXPERTISE`. |
|
|
||||||
| `damageTypes`, `specialdamage` | Immunities / Resistances / Vulnerabilities | `Set<String>` | Array of `{ "name": "...", "type": "i"\|"r"\|"v" }`. Sorted into immunities (`"i"`), resistances (`"r"`), or vulnerabilities (`"v"`). |
|
|
||||||
| `conditions` | `monster.conditionImmunities` | `Set<String>` | Array of `{ "name": "..." }` |
|
|
||||||
| `languages` | `monster.languages` | `Set<Language>` | Array of `{ "name": "...", "speaks": true\|false }` |
|
|
||||||
| `understandsBut` | `monster.understandsButDescription` | `String` | Language qualifier text |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Preview & Database Persistence
|
|
||||||
|
|
||||||
1. **ViewModel Computation (`MonsterImportViewModel.java`)**:
|
|
||||||
- Computes calculated stat block values:
|
|
||||||
- **AC**: Calculated based on `armorType`, dexterity modifier, shield bonus, natural armor bonus, or custom description.
|
|
||||||
- **HP**: Calculated as `floor(hitDice * ((dieSize + 1) / 2 + conModifier))` where `dieSize` is derived from size (e.g., Medium = d8, Large = d10).
|
|
||||||
- **Dynamic Placeholders**: Replaces placeholders in trait descriptions (e.g., `[STR ATK]`, `[WIS SAVE]`) with computed attack bonuses and spell save DCs based on ability scores and proficiency bonuses.
|
|
||||||
- **Markdown Rendering**: Trait and action descriptions are parsed through `CommonMarkHelper` to format inline HTML/Markdown for display.
|
|
||||||
|
|
||||||
2. **Persistence (`MonsterImportFragment.java`)**:
|
|
||||||
- When the user taps the **Import** action menu button (`R.id.menu_action_import_monster`):
|
|
||||||
1. Assigns a new `UUID` to `monster.id`.
|
|
||||||
2. Calls `MonsterRepository.addMonster(monster)` which executes an `INSERT` into the Room database asynchronously on an IO thread via RxJava3.
|
|
||||||
3. Shows a `Snackbar` confirming import success and navigates to the library/detail view for the imported monster.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Internal Database Schema (`monsters` Table)
|
|
||||||
|
|
||||||
The app uses **Room Database** (Database Name: `monsters`, Version: `5`).
|
|
||||||
|
|
||||||
### Table: `monsters`
|
|
||||||
|
|
||||||
| Column Name | SQLite Data Type | Default Value | Description / Room TypeConverter |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| `id` | `TEXT` | Primary Key | UUID string (`UUIDConverter`) |
|
|
||||||
| `name` | `TEXT` | `""` | Monster name |
|
|
||||||
| `size` | `TEXT` | `""` | Size category (e.g. `"medium"`) |
|
|
||||||
| `type` | `TEXT` | `""` | Creature type |
|
|
||||||
| `subtype` | `TEXT` | `""` | Creature tag / subtype |
|
|
||||||
| `alignment` | `TEXT` | `""` | Alignment |
|
|
||||||
| `strength_score` | `INTEGER` | `10` | Strength score |
|
|
||||||
| `strength_saving_throw_advantage` | `TEXT` | `"none"` | Advantage type (`"none"`, `"advantage"`, `"disadvantage"`) |
|
|
||||||
| `strength_saving_throw_proficiency` | `TEXT` | `"none"` | Proficiency type (`"none"`, `"proficient"`, `"expertise"`) |
|
|
||||||
| `dexterity_score` | `INTEGER` | `10` | Dexterity score |
|
|
||||||
| `dexterity_saving_throw_advantage` | `TEXT` | `"none"` | Dexterity saving throw advantage |
|
|
||||||
| `dexterity_saving_throw_proficiency` | `TEXT` | `"none"` | Dexterity saving throw proficiency |
|
|
||||||
| `constitution_score` | `INTEGER` | `10` | Constitution score |
|
|
||||||
| `constitution_saving_throw_advantage` | `TEXT` | `"none"` | Constitution saving throw advantage |
|
|
||||||
| `constitution_saving_throw_proficiency` | `TEXT` | `"none"` | Constitution saving throw proficiency |
|
|
||||||
| `intelligence_score` | `INTEGER` | `10` | Intelligence score |
|
|
||||||
| `intelligence_saving_throw_advantage` | `TEXT` | `"none"` | Intelligence saving throw advantage |
|
|
||||||
| `intelligence_saving_throw_proficiency` | `TEXT` | `"none"` | Intelligence saving throw proficiency |
|
|
||||||
| `wisdom_score` | `INTEGER` | `10` | Wisdom score |
|
|
||||||
| `wisdom_saving_throw_advantage` | `TEXT` | `"none"` | Wisdom saving throw advantage |
|
|
||||||
| `wisdom_saving_throw_proficiency` | `TEXT` | `"none"` | Wisdom saving throw proficiency |
|
|
||||||
| `charisma_score` | `INTEGER` | `10` | Charisma score |
|
|
||||||
| `charisma_saving_throw_advantage` | `TEXT` | `"none"` | Charisma saving throw advantage |
|
|
||||||
| `charisma_saving_throw_proficiency` | `TEXT` | `"none"` | Charisma saving throw proficiency |
|
|
||||||
| `armor_type` | `TEXT` | `"none"` | String representation of `ArmorType` (`ArmorTypeConverter`) |
|
|
||||||
| `shield_bonus` | `INTEGER` | `0` | Shield AC bonus |
|
|
||||||
| `natural_armor_bonus` | `INTEGER` | `0` | Natural armor bonus |
|
|
||||||
| `other_armor_description` | `TEXT` | `""` | Custom armor description |
|
|
||||||
| `hit_dice` | `INTEGER` | `1` | Hit dice count |
|
|
||||||
| `has_custom_hit_points` | `INTEGER` | `0` | Boolean flag (0 = false, 1 = true) |
|
|
||||||
| `custom_hit_points_description` | `TEXT` | `""` | Override HP string |
|
|
||||||
| `walk_speed` | `INTEGER` | `0` | Walk speed in ft |
|
|
||||||
| `burrow_speed` | `INTEGER` | `0` | Burrow speed in ft |
|
|
||||||
| `climb_speed` | `INTEGER` | `0` | Climb speed in ft |
|
|
||||||
| `fly_speed` | `INTEGER` | `0` | Fly speed in ft |
|
|
||||||
| `can_hover` | `INTEGER` | `0` | Boolean flag |
|
|
||||||
| `swim_speed` | `INTEGER` | `0` | Swim speed in ft |
|
|
||||||
| `has_custom_speed` | `INTEGER` | `0` | Boolean flag |
|
|
||||||
| `custom_speed_description` | `TEXT` | NULL | Override speed string |
|
|
||||||
| `challenge_rating` | `TEXT` | `"1"` | String representation of `ChallengeRating` (`ChallengeRatingConverter`) |
|
|
||||||
| `custom_challenge_rating_description` | `TEXT` | `""` | Custom CR text |
|
|
||||||
| `custom_proficiency_bonus` | `INTEGER` | `0` | Custom proficiency bonus override |
|
|
||||||
| `telepathy_range` | `INTEGER` | `0` | Telepathy range in ft |
|
|
||||||
| `understands_but_description` | `TEXT` | `""` | Language qualifier description |
|
|
||||||
| `senses` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
|
|
||||||
| `skills` | `TEXT` | `"[]"` | JSON array of Skill objects (`SetOfSkillConverter`) |
|
|
||||||
| `damage_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
|
|
||||||
| `damage_resistances` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
|
|
||||||
| `damage_vulnerabilities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
|
|
||||||
| `condition_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
|
|
||||||
| `languages` | `TEXT` | `"[]"` | JSON array of Language objects (`SetOfLanguageConverter`) |
|
|
||||||
| `abilities` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
| `actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
| `reactions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
| `lair_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
| `legendary_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
| `regional_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
|
|
||||||
|
|
||||||
### Virtual FTS Table: `monsters_fts`
|
|
||||||
|
|
||||||
Used for full-text search indexing:
|
|
||||||
```sql
|
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts`
|
|
||||||
USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Component Objects Schema (JSON Serialized in DB)
|
|
||||||
|
|
||||||
### 1. `Trait`
|
|
||||||
Used in `abilities`, `actions`, `reactions`, `lair_actions`, `legendary_actions`, and `regional_actions`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Keen Smell",
|
|
||||||
"description": "The monster has advantage on Wisdom (Perception) checks that rely on smell."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. `Skill`
|
|
||||||
Used in `skills`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Perception",
|
|
||||||
"abilityScore": "WISDOM",
|
|
||||||
"advantageType": "NONE",
|
|
||||||
"proficiencyType": "PROFICIENT"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **`abilityScore`**: `"STRENGTH"`, `"DEXTERITY"`, `"CONSTITUTION"`, `"INTELLIGENCE"`, `"WISDOM"`, `"CHARISMA"`.
|
|
||||||
- **`advantageType`**: `"NONE"`, `"ADVANTAGE"`, `"DISADVANTAGE"`.
|
|
||||||
- **`proficiencyType`**: `"NONE"`, `"PROFICIENT"`, `"EXPERTISE"`.
|
|
||||||
|
|
||||||
### 3. `Language`
|
|
||||||
Used in `languages`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mName": "Common",
|
|
||||||
"mSpeaks": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **`mName`**: Name of the language (e.g., `"Elvish"`).
|
|
||||||
- **`mSpeaks`**: `true` if the creature can speak it; `false` if it only understands it.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Ancient Black Dragon","size":"gargantuan","type":"Dragon","tag":"","alignment":"chaotic evil","hitDice":"21","armorName":"natural armor","shieldBonus":0,"natArmorBonus":10,"otherArmorDesc":"22 (natural armor)","speed":"40","burrowSpeed":"0","climbSpeed":"0","flySpeed":"80","hover":false,"swimSpeed":"40","customHP":false,"customSpeed":false,"hpText":"367 (21d20 + 147)","speedDesc":"40 ft., fly 80 ft., swim 40 ft.","strPoints":"27","dexPoints":"14","conPoints":"25","intPoints":"16","wisPoints":"15","chaPoints":"19","blindsight":"60","blind":false,"darkvision":"120","tremorsense":"0","truesight":"0","telepathy":0,"cr":"21","customCr":"21 (33,000 XP)","customProf":7,"isLegendary":true,"legendariesDescription":"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.","isLair":true,"lairDescription":"When fighting inside its lair, the ancient black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the ancient black dragon can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":true,"mythicDescription":"If the ancient black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":true,"regionalDescription":"The region containing the ancient black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the ancient black dragon dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Amphibious","desc":"The dragon can breathe air and water."},{"name":"Legendary Resistance (3/Day)","desc":"If the dragon fails a saving throw, it can choose to succeed instead."}],"actions":[{"name":"Multiattack","desc":"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."},{"name":"Bite","desc":"_Melee Weapon Attack:_ +15 to hit, reach 15 ft., one target. _Hit:_ 19 (2d10 + 8) piercing damage plus 9 (2d8) acid damage."},{"name":"Claw","desc":"_Melee Weapon Attack:_ +15 to hit, reach 10 ft., one target. _Hit:_ 15 (2d6 + 8) slashing damage."},{"name":"Tail","desc":"_Melee Weapon Attack:_ +15 to hit, reach 20 ft., one target. _Hit:_ 17 (2d8 + 8) bludgeoning damage."},{"name":"Frightful Presence","desc":"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."},{"name":"Acid Breath (Recharge 5-6)","desc":"The dragon exhales acid in a 90-foot line that is 10 feet wide. Each creature in that line must make a DC 22 Dexterity saving throw, taking 67 (15d8) acid damage on a failed save, or half as much damage on a successful one."}],"bonusActions":[],"reactions":[],"legendaries":[{"name":"Detect","desc":"The dragon makes a Wisdom (Perception) check."},{"name":"Tail Attack","desc":"The dragon makes a tail attack."},{"name":"Wing Attack (Costs 2 Actions)","desc":"The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 23 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."}],"mythics":[],"lairs":[],"regionals":[],"sthrows":[{"name":"dex","order":1},{"name":"con","order":2},{"name":"wis","order":4},{"name":"cha","order":5}],"skills":[{"name":"perception","stat":"wis","note":" (ex)"},{"name":"stealth","stat":"dex"}],"damagetypes":[{"name":"acid","note":" (Immune)","type":"i"}],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Draconic","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":5,"damage":[]}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"name":"Acolyte","size":"medium","type":"Humanoid","tag":"any race","alignment":"any alignment","hitDice":2,"armorName":"none","shieldBonus":0,"natArmorBonus":7,"otherArmorDesc":"10","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":10,"dexPoints":10,"conPoints":10,"intPoints":10,"wisPoints":14,"chaPoints":11,"blindsight":0,"blind":false,"darkvision":0,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The acolyte can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The acolyte regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the acolyte can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the acolyte can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The acolyte can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the acolyte's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the acolyte's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the acolyte dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Spellcasting","desc":"The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> * Cantrips (at will): _light, sacred flame, thaumaturgy_\n> * 1st level (3 slots): _bless, cure wounds, sanctuary_"}],"actions":[{"name":"Club","desc":"_Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"medicine","stat":"wis"},{"name":"religion","stat":"int"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"any one language (usually Common)","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":0,"damage":[]}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
Homebrewery V3
|
|
||||||
{{monster,frame
|
|
||||||
## Acolyte
|
|
||||||
*Medium Humanoid (any race), any alignment*
|
|
||||||
___
|
|
||||||
**Armor Class** :: 10
|
|
||||||
**Hit Points** :: 9 (2d8)
|
|
||||||
**Speed** :: 30 ft.
|
|
||||||
___
|
|
||||||
|STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
|10 (+0)|10 (+0)|10 (+0)|10 (+0)|14 (+2)|11 (+0)|
|
|
||||||
___
|
|
||||||
**Skills** :: Medicine +4, Religion +2
|
|
||||||
**Senses** :: passive Perception 12
|
|
||||||
**Languages** :: any one language (usually Common)
|
|
||||||
**Challenge** :: 1/4 (50 XP)
|
|
||||||
___
|
|
||||||
***Spellcasting.*** The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC 12, +4 to hit with spell attacks). The acolyte has following cleric spells prepared:<br>
|
|
||||||
<br>
|
|
||||||
* Cantrips (at will): _light, sacred flame, thaumaturgy_<br>
|
|
||||||
* 1st level (3 slots): _bless, cure wounds, sanctuary_
|
|
||||||
### Actions
|
|
||||||
***Club.*** _Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage.
|
|
||||||
}}
|
|
||||||
Homebrewery (Legacy)/GM Binder Markdown
|
|
||||||
___
|
|
||||||
> ## Acolyte
|
|
||||||
> *Medium Humanoid (any race), any alignment*
|
|
||||||
> ___
|
|
||||||
> - **Armor Class** 10
|
|
||||||
> - **Hit Points** 9 (2d8)
|
|
||||||
> - **Speed** 30 ft.
|
|
||||||
> ___
|
|
||||||
> |STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
> |:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
> |10 (+0)|10 (+0)|10 (+0)|10 (+0)|14 (+2)|11 (+0)|
|
|
||||||
> ___
|
|
||||||
> - **Skills** Medicine +4, Religion +2
|
|
||||||
> - **Senses** passive Perception 12
|
|
||||||
> - **Languages** any one language (usually Common)
|
|
||||||
> - **Challenge** 1/4 (50 XP)
|
|
||||||
> ___
|
|
||||||
> ***Spellcasting.*** The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC 12, +4 to hit with spell attacks). The acolyte has following cleric spells prepared:<br>
|
|
||||||
> <br>
|
|
||||||
> * Cantrips (at will): _light, sacred flame, thaumaturgy_<br>
|
|
||||||
> * 1st level (3 slots): _bless, cure wounds, sanctuary_
|
|
||||||
> ### Actions
|
|
||||||
> ***Club.*** _Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Acolyte","size":"medium","type":"Humanoid","tag":"any race","alignment":"any alignment","hitDice":2,"armorName":"none","shieldBonus":0,"natArmorBonus":7,"otherArmorDesc":"10","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":10,"dexPoints":10,"conPoints":10,"intPoints":10,"wisPoints":14,"chaPoints":11,"blindsight":0,"blind":false,"darkvision":0,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The acolyte can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The acolyte regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the acolyte can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the acolyte can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The acolyte can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the acolyte's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the acolyte's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the acolyte dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Spellcasting","desc":"The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> * Cantrips (at will): _light, sacred flame, thaumaturgy_\n> * 1st level (3 slots): _bless, cure wounds, sanctuary_"}],"actions":[{"name":"Club","desc":"_Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"medicine","stat":"wis"},{"name":"religion","stat":"int"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"any one language (usually Common)","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":0,"damage":[]}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB |
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"name":"Adult Black Dragon","size":"huge","type":"Dragon","tag":"","alignment":"chaotic evil","hitDice":17,"armorName":"natural armor","shieldBonus":0,"natArmorBonus":7,"otherArmorDesc":"19 (natural armor)","speed":40,"burrowSpeed":0,"climbSpeed":0,"flySpeed":80,"hover":false,"swimSpeed":40,"customHP":false,"customSpeed":false,"hpText":"17","speedDesc":"40 ft., fly 80 ft., swim 40 ft.","strPoints":23,"dexPoints":14,"conPoints":21,"intPoints":14,"wisPoints":13,"chaPoints":17,"blindsight":60,"blind":false,"darkvision":120,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"14","customCr":"14 (11,500 XP)","customProf":5,"isLegendary":true,"legendariesDescription":"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the adult black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the adult black dragon can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The adult black dragon can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the adult black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the adult black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the adult black dragon dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Amphibious","desc":"The dragon can breathe air and water."},{"name":"Legendary Resistance (3/Day)","desc":"If the dragon fails a saving throw, it can choose to succeed instead."}],"actions":[{"name":"Multiattack","desc":"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."},{"name":"Bite","desc":"_Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage."},{"name":"Claw","desc":"_Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage."},{"name":"Tail","desc":"_Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage."},{"name":"Frightful Presence","desc":"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."},{"name":"Acid Breath (Recharge 5-6)","desc":"The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one."}],"bonusActions":[],"reactions":[],"legendaries":[{"name":"Detect","desc":"The dragon makes a Wisdom (Perception) check."},{"name":"Tail Attack","desc":"The dragon makes a tail attack."},{"name":"Wing Attack (Costs 2 Actions)","desc":"The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."}],"mythics":[],"lairs":[],"regionals":[],"sthrows":[{"name":"dex","order":1},{"name":"con","order":2},{"name":"wis","order":4},{"name":"cha","order":5}],"skills":[{"name":"perception","stat":"wis","note":" (ex)"},{"name":"stealth","stat":"dex"}],"damagetypes":[{"name":"acid","note":" (Immune)","type":"i"}],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Draconic","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":5,"damage":[]}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
Homebrewery V3
|
|
||||||
{{monster,frame
|
|
||||||
## Adult Black Dragon
|
|
||||||
*Huge Dragon, chaotic evil*
|
|
||||||
___
|
|
||||||
**Armor Class** :: 19 (natural armor)
|
|
||||||
**Hit Points** :: 195 (17d12 + 85)
|
|
||||||
**Speed** :: 40 ft., fly 80 ft., swim 40 ft.
|
|
||||||
___
|
|
||||||
|STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
|23 (+6)|14 (+2)|21 (+5)|14 (+2)|13 (+1)|17 (+3)|
|
|
||||||
___
|
|
||||||
**Saving Throws** :: Dex +7, Con +10, Wis +6, Cha +8
|
|
||||||
**Skills** :: Perception +11, Stealth +7
|
|
||||||
**Damage Immunities** :: acid
|
|
||||||
**Senses** :: blindsight 60 ft., darkvision 120 ft., passive Perception 21
|
|
||||||
**Languages** :: Common, Draconic
|
|
||||||
**Challenge** :: 14 (11,500 XP)
|
|
||||||
___
|
|
||||||
***Amphibious.*** The dragon can breathe air and water.
|
|
||||||
:
|
|
||||||
***Legendary Resistance (3/Day).*** If the dragon fails a saving throw, it can choose to succeed instead.
|
|
||||||
### Actions
|
|
||||||
***Multiattack.*** The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.
|
|
||||||
:
|
|
||||||
***Bite.*** _Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage.
|
|
||||||
:
|
|
||||||
***Claw.*** _Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage.
|
|
||||||
:
|
|
||||||
***Tail.*** _Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage.
|
|
||||||
:
|
|
||||||
***Frightful Presence.*** Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours.
|
|
||||||
:
|
|
||||||
***Acid Breath (Recharge 5-6).*** The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one.
|
|
||||||
### Legendary Actions
|
|
||||||
The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.
|
|
||||||
:
|
|
||||||
**Detect.** The dragon makes a Wisdom (Perception) check.
|
|
||||||
:
|
|
||||||
**Tail Attack.** The dragon makes a tail attack.
|
|
||||||
:
|
|
||||||
**Wing Attack (Costs 2 Actions).** The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.
|
|
||||||
}}
|
|
||||||
Homebrewery (Legacy)/GM Binder Markdown
|
|
||||||
___
|
|
||||||
> ## Adult Black Dragon
|
|
||||||
> *Huge Dragon, chaotic evil*
|
|
||||||
> ___
|
|
||||||
> - **Armor Class** 19 (natural armor)
|
|
||||||
> - **Hit Points** 195 (17d12 + 85)
|
|
||||||
> - **Speed** 40 ft., fly 80 ft., swim 40 ft.
|
|
||||||
> ___
|
|
||||||
> |STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
> |:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
> |23 (+6)|14 (+2)|21 (+5)|14 (+2)|13 (+1)|17 (+3)|
|
|
||||||
> ___
|
|
||||||
> - **Saving Throws** Dex +7, Con +10, Wis +6, Cha +8
|
|
||||||
> - **Skills** Perception +11, Stealth +7
|
|
||||||
> - **Damage Immunities** acid
|
|
||||||
> - **Senses** blindsight 60 ft., darkvision 120 ft., passive Perception 21
|
|
||||||
> - **Languages** Common, Draconic
|
|
||||||
> - **Challenge** 14 (11,500 XP)
|
|
||||||
> ___
|
|
||||||
> ***Amphibious.*** The dragon can breathe air and water.
|
|
||||||
>
|
|
||||||
> ***Legendary Resistance (3/Day).*** If the dragon fails a saving throw, it can choose to succeed instead.
|
|
||||||
> ### Actions
|
|
||||||
> ***Multiattack.*** The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.
|
|
||||||
>
|
|
||||||
> ***Bite.*** _Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage.
|
|
||||||
>
|
|
||||||
> ***Claw.*** _Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage.
|
|
||||||
>
|
|
||||||
> ***Tail.*** _Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage.
|
|
||||||
>
|
|
||||||
> ***Frightful Presence.*** Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours.
|
|
||||||
>
|
|
||||||
> ***Acid Breath (Recharge 5-6).*** The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one.
|
|
||||||
> ### Legendary Actions
|
|
||||||
> The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.
|
|
||||||
>
|
|
||||||
> **Detect.** The dragon makes a Wisdom (Perception) check.
|
|
||||||
>
|
|
||||||
> **Tail Attack.** The dragon makes a tail attack.
|
|
||||||
>
|
|
||||||
> **Wing Attack (Costs 2 Actions).** The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Adult Black Dragon","size":"huge","type":"Dragon","tag":"","alignment":"chaotic evil","hitDice":17,"armorName":"natural armor","shieldBonus":0,"natArmorBonus":7,"otherArmorDesc":"19 (natural armor)","speed":40,"burrowSpeed":0,"climbSpeed":0,"flySpeed":80,"hover":false,"swimSpeed":40,"customHP":false,"customSpeed":false,"hpText":"17","speedDesc":"40 ft., fly 80 ft., swim 40 ft.","strPoints":23,"dexPoints":14,"conPoints":21,"intPoints":14,"wisPoints":13,"chaPoints":17,"blindsight":60,"blind":false,"darkvision":120,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"14","customCr":"14 (11,500 XP)","customProf":5,"isLegendary":true,"legendariesDescription":"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the adult black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the adult black dragon can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The adult black dragon can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the adult black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the adult black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the adult black dragon dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Amphibious","desc":"The dragon can breathe air and water."},{"name":"Legendary Resistance (3/Day)","desc":"If the dragon fails a saving throw, it can choose to succeed instead."}],"actions":[{"name":"Multiattack","desc":"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."},{"name":"Bite","desc":"_Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage."},{"name":"Claw","desc":"_Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage."},{"name":"Tail","desc":"_Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage."},{"name":"Frightful Presence","desc":"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."},{"name":"Acid Breath (Recharge 5-6)","desc":"The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one."}],"bonusActions":[],"reactions":[],"legendaries":[{"name":"Detect","desc":"The dragon makes a Wisdom (Perception) check."},{"name":"Tail Attack","desc":"The dragon makes a tail attack."},{"name":"Wing Attack (Costs 2 Actions)","desc":"The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."}],"mythics":[],"lairs":[],"regionals":[],"sthrows":[{"name":"dex","order":1},{"name":"con","order":2},{"name":"wis","order":4},{"name":"cha","order":5}],"skills":[{"name":"perception","stat":"wis","note":" (ex)"},{"name":"stealth","stat":"dex"}],"damagetypes":[{"name":"acid","note":" (Immune)","type":"i"}],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Draconic","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":5,"damage":[]}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 809 KiB |
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"name":"Monster Goblin","size":"small","type":"Humanoid","tag":"goblinoid","alignment":"neutral evil","hitDice":2,"armorName":"leather armor","shieldBonus":2,"natArmorBonus":7,"otherArmorDesc":"15 (leather armor, shield)","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":8,"dexPoints":14,"conPoints":10,"intPoints":10,"wisPoints":8,"chaPoints":8,"blindsight":0,"blind":false,"darkvision":60,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The goblin can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The goblin regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the goblin can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the goblin can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the goblin's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the goblin's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the goblin dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Nimble Escape","desc":"The goblin can take the Disengage or Hide action as a bonus action on each of its turns."}],"actions":[{"name":"Scimitar","desc":"_Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage."},{"name":"Shortbow","desc":"_Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"stealth","stat":"dex","note":" (ex)"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Goblin","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":1,"damage":[]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Text Monster Goblin","size":"small","type":"Humanoid","tag":"goblinoid","alignment":"neutral evil","hitDice":2,"armorName":"leather armor","shieldBonus":2,"natArmorBonus":7,"otherArmorDesc":"15 (leather armor, shield)","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":8,"dexPoints":14,"conPoints":10,"intPoints":10,"wisPoints":8,"chaPoints":8,"blindsight":0,"blind":false,"darkvision":60,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The goblin can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The goblin regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the goblin can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the goblin can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the goblin's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the goblin's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the goblin dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Nimble Escape","desc":"The goblin can take the Disengage or Hide action as a bonus action on each of its turns."}],"actions":[{"name":"Scimitar","desc":"_Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage."},{"name":"Shortbow","desc":"_Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"stealth","stat":"dex","note":" (ex)"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Goblin","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":1,"damage":[]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Text Goblin","size":"small","type":"Humanoid","tag":"goblinoid","alignment":"neutral evil","hitDice":2,"armorName":"leather armor","shieldBonus":2,"natArmorBonus":7,"otherArmorDesc":"15 (leather armor, shield)","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":8,"dexPoints":14,"conPoints":10,"intPoints":10,"wisPoints":8,"chaPoints":8,"blindsight":0,"blind":false,"darkvision":60,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The goblin can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The goblin regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the goblin can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the goblin can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the goblin's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the goblin's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the goblin dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Nimble Escape","desc":"The goblin can take the Disengage or Hide action as a bonus action on each of its turns."}],"actions":[{"name":"Scimitar","desc":"_Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage."},{"name":"Shortbow","desc":"_Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"stealth","stat":"dex","note":" (ex)"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Goblin","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":1,"damage":[]}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
Homebrewery V3
|
|
||||||
{{monster,frame
|
|
||||||
## Goblin
|
|
||||||
*Small Humanoid (goblinoid), neutral evil*
|
|
||||||
___
|
|
||||||
**Armor Class** :: 15 (leather armor, shield)
|
|
||||||
**Hit Points** :: 7 (2d6)
|
|
||||||
**Speed** :: 30 ft.
|
|
||||||
___
|
|
||||||
|STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
|8 (-1)|14 (+2)|10 (+0)|10 (+0)|8 (-1)|8 (-1)|
|
|
||||||
___
|
|
||||||
**Skills** :: Stealth +6
|
|
||||||
**Senses** :: darkvision 60 ft., passive Perception 9
|
|
||||||
**Languages** :: Common, Goblin
|
|
||||||
**Challenge** :: 1/4 (50 XP)
|
|
||||||
___
|
|
||||||
***Nimble Escape.*** The goblin can take the Disengage or Hide action as a bonus action on each of its turns.
|
|
||||||
### Actions
|
|
||||||
***Scimitar.*** _Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage.
|
|
||||||
:
|
|
||||||
***Shortbow.*** _Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage.
|
|
||||||
}}
|
|
||||||
Homebrewery (Legacy)/GM Binder Markdown
|
|
||||||
___
|
|
||||||
> ## Goblin
|
|
||||||
> *Small Humanoid (goblinoid), neutral evil*
|
|
||||||
> ___
|
|
||||||
> - **Armor Class** 15 (leather armor, shield)
|
|
||||||
> - **Hit Points** 7 (2d6)
|
|
||||||
> - **Speed** 30 ft.
|
|
||||||
> ___
|
|
||||||
> |STR|DEX|CON|INT|WIS|CHA|
|
|
||||||
> |:---:|:---:|:---:|:---:|:---:|:---:|
|
|
||||||
> |8 (-1)|14 (+2)|10 (+0)|10 (+0)|8 (-1)|8 (-1)|
|
|
||||||
> ___
|
|
||||||
> - **Skills** Stealth +6
|
|
||||||
> - **Senses** darkvision 60 ft., passive Perception 9
|
|
||||||
> - **Languages** Common, Goblin
|
|
||||||
> - **Challenge** 1/4 (50 XP)
|
|
||||||
> ___
|
|
||||||
> ***Nimble Escape.*** The goblin can take the Disengage or Hide action as a bonus action on each of its turns.
|
|
||||||
> ### Actions
|
|
||||||
> ***Scimitar.*** _Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage.
|
|
||||||
>
|
|
||||||
> ***Shortbow.*** _Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Goblin","size":"small","type":"Humanoid","tag":"goblinoid","alignment":"neutral evil","hitDice":2,"armorName":"leather armor","shieldBonus":2,"natArmorBonus":7,"otherArmorDesc":"15 (leather armor, shield)","speed":30,"burrowSpeed":0,"climbSpeed":0,"flySpeed":0,"hover":false,"swimSpeed":0,"customHP":false,"customSpeed":false,"hpText":"2","speedDesc":"30 ft.","strPoints":8,"dexPoints":14,"conPoints":10,"intPoints":10,"wisPoints":8,"chaPoints":8,"blindsight":0,"blind":false,"darkvision":60,"tremorsense":0,"truesight":0,"telepathy":0,"cr":"1/4","customCr":"1/4 (50 XP)","customProf":2,"isLegendary":false,"legendariesDescription":"The goblin can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The goblin regains spent legendary actions at the start of its turn.","isLair":false,"lairDescription":"When fighting inside its lair, the goblin can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the goblin can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":false,"mythicDescription":"If the goblin's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":false,"regionalDescription":"The region containing the goblin's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the goblin dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Nimble Escape","desc":"The goblin can take the Disengage or Hide action as a bonus action on each of its turns."}],"actions":[{"name":"Scimitar","desc":"_Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage."},{"name":"Shortbow","desc":"_Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage."}],"bonusActions":[],"reactions":[],"legendaries":[],"mythics":[],"lairs":[],"regionals":[],"sthrows":[],"skills":[{"name":"stealth","stat":"dex","note":" (ex)"}],"damagetypes":[],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Goblin","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":1,"damage":[]}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 365 KiB |
Binary file not shown.
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../../../schema/entity.json",
|
|
||||||
"uuid": "f6a83d72-b94e-4e5c-a5c2-f39b6b52d202",
|
|
||||||
"ruleset_id": "open5e",
|
|
||||||
"entity_type": "character",
|
|
||||||
"display_name": "Acolyte",
|
|
||||||
"description": "Medium humanoid (any race), any alignment.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"template_id": "stat_block",
|
|
||||||
"tags": ["npc", "humanoid", "spellcaster", "cr-1/4"],
|
|
||||||
"properties": {
|
|
||||||
"size": "medium",
|
|
||||||
"type": "Humanoid",
|
|
||||||
"subtype": "any race",
|
|
||||||
"alignment": "any alignment",
|
|
||||||
"armor_class": 10,
|
|
||||||
"hit_points": {
|
|
||||||
"current": 9,
|
|
||||||
"max": 9,
|
|
||||||
"formula": "2d8 + 0",
|
|
||||||
"hit_dice": "2d8"
|
|
||||||
},
|
|
||||||
"speed": 30,
|
|
||||||
"speed_desc": "30 ft.",
|
|
||||||
"challenge_rating": "1/4 (50 XP)",
|
|
||||||
"cr": "1/4",
|
|
||||||
"abilities": {
|
|
||||||
"strength": 10,
|
|
||||||
"dexterity": 10,
|
|
||||||
"constitution": 10,
|
|
||||||
"intelligence": 10,
|
|
||||||
"wisdom": 14,
|
|
||||||
"charisma": 11
|
|
||||||
},
|
|
||||||
"skills": ["medicine +4", "religion +2"],
|
|
||||||
"senses": "passive Perception 12",
|
|
||||||
"languages": ["any one language (usually Common)"],
|
|
||||||
"traits": [
|
|
||||||
{
|
|
||||||
"name": "Spellcasting",
|
|
||||||
"desc": "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC 12, +4 to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n* Cantrips (at will): _light, sacred flame, thaumaturgy_\n* 1st level (3 slots): _bless, cure wounds, sanctuary_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"name": "Club",
|
|
||||||
"desc": "_Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../../../schema/entity.json",
|
|
||||||
"uuid": "b8a91c34-d07f-4f8a-92c1-d41a8a91e404",
|
|
||||||
"ruleset_id": "open5e",
|
|
||||||
"entity_type": "character",
|
|
||||||
"display_name": "Adult Black Dragon",
|
|
||||||
"description": "Huge dragon, chaotic evil.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"template_id": "stat_block",
|
|
||||||
"tags": ["monster", "dragon", "legendary", "cr-14"],
|
|
||||||
"properties": {
|
|
||||||
"size": "huge",
|
|
||||||
"type": "Dragon",
|
|
||||||
"alignment": "chaotic evil",
|
|
||||||
"armor_class": 19,
|
|
||||||
"armor_description": "natural armor",
|
|
||||||
"hit_points": {
|
|
||||||
"current": 196,
|
|
||||||
"max": 196,
|
|
||||||
"formula": "17d12 + 85",
|
|
||||||
"hit_dice": "17d12"
|
|
||||||
},
|
|
||||||
"speed": 40,
|
|
||||||
"speed_desc": "40 ft., fly 80 ft., swim 40 ft.",
|
|
||||||
"challenge_rating": "14 (11,500 XP)",
|
|
||||||
"cr": "14",
|
|
||||||
"abilities": {
|
|
||||||
"strength": 23,
|
|
||||||
"dexterity": 14,
|
|
||||||
"constitution": 21,
|
|
||||||
"intelligence": 14,
|
|
||||||
"wisdom": 13,
|
|
||||||
"charisma": 17
|
|
||||||
},
|
|
||||||
"saving_throws": [
|
|
||||||
"Dexterity +7",
|
|
||||||
"Constitution +10",
|
|
||||||
"Wisdom +6",
|
|
||||||
"Charisma +8"
|
|
||||||
],
|
|
||||||
"skills": ["perception +16", "stealth +11"],
|
|
||||||
"damage_immunities": ["acid"],
|
|
||||||
"senses": "darkvision 120 ft., blindsight 60 ft., passive Perception 11",
|
|
||||||
"languages": ["Common", "Draconic"],
|
|
||||||
"traits": [
|
|
||||||
{
|
|
||||||
"name": "Amphibious",
|
|
||||||
"desc": "The dragon can breathe air and water."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Legendary Resistance (3/Day)",
|
|
||||||
"desc": "If the dragon fails a saving throw, it can choose to succeed instead."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"name": "Multiattack",
|
|
||||||
"desc": "The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Bite",
|
|
||||||
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Claw",
|
|
||||||
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Tail",
|
|
||||||
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Frightful Presence",
|
|
||||||
"desc": "Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Acid Breath (Recharge 5-6)",
|
|
||||||
"desc": "The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"legendary_actions": {
|
|
||||||
"description": "The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"name": "Detect",
|
|
||||||
"desc": "The dragon makes a Wisdom (Perception) check."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Tail Attack",
|
|
||||||
"desc": "The dragon makes a tail attack."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Wing Attack (Costs 2 Actions)",
|
|
||||||
"desc": "The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user