diff --git a/Android/.gitignore b/Android/.gitignore
new file mode 100644
index 0000000..a840822
--- /dev/null
+++ b/Android/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+/.idea/dictionaries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+/app/debug
+/app/release
diff --git a/Android/.idea/.name b/Android/.idea/.name
new file mode 100644
index 0000000..c136dab
--- /dev/null
+++ b/Android/.idea/.name
@@ -0,0 +1 @@
+MonsterCards
\ No newline at end of file
diff --git a/Android/.idea/codeStyles/Project.xml b/Android/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..681f41a
--- /dev/null
+++ b/Android/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/compiler.xml b/Android/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/Android/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/gradle.xml b/Android/.idea/gradle.xml
new file mode 100644
index 0000000..23a89bb
--- /dev/null
+++ b/Android/.idea/gradle.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/inspectionProfiles/Project_Default.xml b/Android/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..5d018a6
--- /dev/null
+++ b/Android/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/jarRepositories.xml b/Android/.idea/jarRepositories.xml
new file mode 100644
index 0000000..e34606c
--- /dev/null
+++ b/Android/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/misc.xml b/Android/.idea/misc.xml
new file mode 100644
index 0000000..6c2e59d
--- /dev/null
+++ b/Android/.idea/misc.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/runConfigurations.xml b/Android/.idea/runConfigurations.xml
new file mode 100644
index 0000000..e497da9
--- /dev/null
+++ b/Android/.idea/runConfigurations.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/saveactions_settings.xml b/Android/.idea/saveactions_settings.xml
new file mode 100644
index 0000000..033b46c
--- /dev/null
+++ b/Android/.idea/saveactions_settings.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/.idea/vcs.xml b/Android/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/Android/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/README.md b/Android/README.md
new file mode 100644
index 0000000..430f62e
--- /dev/null
+++ b/Android/README.md
@@ -0,0 +1,4 @@
+[](https://appcenter.ms)
+
+# MonsterCards for Android
+
diff --git a/Android/app/.gitignore b/Android/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/Android/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Android/app/build.gradle b/Android/app/build.gradle
new file mode 100644
index 0000000..1d8cad7
--- /dev/null
+++ b/Android/app/build.gradle
@@ -0,0 +1,109 @@
+plugins {
+ id 'com.android.application'
+ id 'androidx.navigation.safeargs'
+}
+
+Properties properties = new Properties()
+def propertiesFile = project.rootProject.file('local.properties')
+if (propertiesFile.exists()) {
+ properties.load(propertiesFile.newDataInputStream())
+}
+def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
+def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
+def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
+def appCenterSdkVersion = '3.3.0'
+def nav_version = '2.3.5'
+def room_version = '2.3.0'
+def rxjava_version = '3.0.0'
+def flipper_version = '0.87.0'
+def soloader_version = '0.10.1'
+def gson_version = '2.8.6'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion '30.0.3'
+
+ defaultConfig {
+ applicationId "com.majinnaibu.monstercards"
+ minSdkVersion 22
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+ buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ // Enables code shrinking, obfuscation, and optimization for only
+ // your project's release build type.
+ minifyEnabled true
+
+ // Enables resource shrinking, which is performed by the
+ // Android Gradle plugin.
+ shrinkResources true
+
+ // Includes the default ProGuard rules files that are packaged with
+ // the Android Gradle plugin. To learn more, go to the section about
+ // R8 configuration files.
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ // Included libs
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+
+ // Google
+ implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'com.google.android.material:material:1.4.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
+ implementation "androidx.navigation:navigation-fragment:$nav_version"
+ implementation "androidx.navigation:navigation-ui:$nav_version"
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
+
+ // Testing
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ // Room DB
+ implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
+ implementation "io.reactivex.rxjava3:rxandroid:$rxjava_version"
+ implementation "androidx.room:room-runtime:$room_version"
+ annotationProcessor "androidx.room:room-compiler:$room_version"
+ implementation "androidx.room:room-rxjava3:$room_version"
+ //testImplementation "androidx.room:room-testing:$room_version"
+
+ // AppCenter
+ debugImplementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
+ debugImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
+
+ // Flipper
+ debugImplementation "com.facebook.flipper:flipper:$flipper_version"
+ debugImplementation "com.facebook.soloader:soloader:$soloader_version"
+ releaseImplementation "com.facebook.flipper:flipper-noop:$flipper_version"
+
+ // Other 3rd Party
+ implementation 'com.atlassian.commonmark:commonmark:0.15.2'
+ implementation "com.google.code.gson:gson:$gson_version"
+}
diff --git a/Android/app/proguard-rules.pro b/Android/app/proguard-rules.pro
new file mode 100644
index 0000000..08e4fd6
--- /dev/null
+++ b/Android/app/proguard-rules.pro
@@ -0,0 +1,32 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-keep enum com.majinnaibu.monstercards.data.enums.AbilityScore
+-keep enum com.majinnaibu.monstercards.data.enums.ProficiencyType
+-keep enum com.majinnaibu.monstercards.data.enums.AdvantageType
+-keep enum com.majinnaibu.monstercards.data.enums.TraitType
+-keep enum com.majinnaibu.monstercards.data.enums.StringType
+-keepclassmembers,allowoptimization enum * {
+ ;
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
diff --git a/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json
new file mode 100644
index 0000000..83ab2bc
--- /dev/null
+++ b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json
@@ -0,0 +1,440 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "db1293d2f490940b55ca1f4f56b21b1a",
+ "entities": [
+ {
+ "tableName": "Monster",
+ "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, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, 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",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "strengthSavingThrowProficiency",
+ "columnName": "strength_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexterityScore",
+ "columnName": "dexterity_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowAdvantage",
+ "columnName": "dexterity_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowProficiency",
+ "columnName": "dexterity_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionScore",
+ "columnName": "constitution_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowAdvantage",
+ "columnName": "constitution_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowProficiency",
+ "columnName": "constitution_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceScore",
+ "columnName": "intelligence_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowAdvantage",
+ "columnName": "intelligence_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowProficiency",
+ "columnName": "intelligence_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomScore",
+ "columnName": "wisdom_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowAdvantage",
+ "columnName": "wisdom_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowProficiency",
+ "columnName": "wisdom_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaScore",
+ "columnName": "charisma_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "charismaSavingThrowAdvantage",
+ "columnName": "charisma_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaSavingThrowProficiency",
+ "columnName": "charisma_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "armorType",
+ "columnName": "armor_type",
+ "affinity": "TEXT",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "challengeRating",
+ "columnName": "challenge_rating",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ },
+ {
+ "fieldPath": "customChallengeRatingDescription",
+ "columnName": "custom_challenge_rating_description",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "customProficiencyBonus",
+ "columnName": "custom_proficiency_bonus",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "blindsightRange",
+ "columnName": "blindsight_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isBlindBeyondBlindsightRange",
+ "columnName": "is_blind_beyond_blindsight_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "darkvisionRange",
+ "columnName": "darkvision_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "tremorsenseRange",
+ "columnName": "tremorsense_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "truesightRange",
+ "columnName": "truesight_range",
+ "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",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "skills",
+ "columnName": "skills",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageImmunities",
+ "columnName": "damage_immunities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageResistances",
+ "columnName": "damage_resistances",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageVulnerabilities",
+ "columnName": "damage_vulnerabilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "conditionImmunities",
+ "columnName": "condition_immunities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "languages",
+ "columnName": "languages",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "abilities",
+ "columnName": "abilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "actions",
+ "columnName": "actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lairActions",
+ "columnName": "lair_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "legendaryActions",
+ "columnName": "legendary_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "regionalActions",
+ "columnName": "regional_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "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, 'db1293d2f490940b55ca1f4f56b21b1a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json
new file mode 100644
index 0000000..fada53b
--- /dev/null
+++ b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json
@@ -0,0 +1,499 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "6f1e7a2b2ab96fc4be4da1657a7a0138",
+ "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, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, 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",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "strengthSavingThrowProficiency",
+ "columnName": "strength_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexterityScore",
+ "columnName": "dexterity_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowAdvantage",
+ "columnName": "dexterity_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowProficiency",
+ "columnName": "dexterity_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionScore",
+ "columnName": "constitution_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowAdvantage",
+ "columnName": "constitution_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowProficiency",
+ "columnName": "constitution_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceScore",
+ "columnName": "intelligence_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowAdvantage",
+ "columnName": "intelligence_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowProficiency",
+ "columnName": "intelligence_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomScore",
+ "columnName": "wisdom_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowAdvantage",
+ "columnName": "wisdom_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowProficiency",
+ "columnName": "wisdom_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaScore",
+ "columnName": "charisma_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "charismaSavingThrowAdvantage",
+ "columnName": "charisma_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaSavingThrowProficiency",
+ "columnName": "charisma_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "armorType",
+ "columnName": "armor_type",
+ "affinity": "TEXT",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "challengeRating",
+ "columnName": "challenge_rating",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ },
+ {
+ "fieldPath": "customChallengeRatingDescription",
+ "columnName": "custom_challenge_rating_description",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "customProficiencyBonus",
+ "columnName": "custom_proficiency_bonus",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "blindsightRange",
+ "columnName": "blindsight_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isBlindBeyondBlindsightRange",
+ "columnName": "is_blind_beyond_blindsight_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "darkvisionRange",
+ "columnName": "darkvision_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "tremorsenseRange",
+ "columnName": "tremorsense_range",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "truesightRange",
+ "columnName": "truesight_range",
+ "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",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "skills",
+ "columnName": "skills",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageImmunities",
+ "columnName": "damage_immunities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageResistances",
+ "columnName": "damage_resistances",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "damageVulnerabilities",
+ "columnName": "damage_vulnerabilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "conditionImmunities",
+ "columnName": "condition_immunities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "languages",
+ "columnName": "languages",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "abilities",
+ "columnName": "abilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "actions",
+ "columnName": "actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lairActions",
+ "columnName": "lair_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "legendaryActions",
+ "columnName": "legendary_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "regionalActions",
+ "columnName": "regional_actions",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "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": "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subtype",
+ "columnName": "subtype",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "alignment",
+ "columnName": "alignment",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "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, '6f1e7a2b2ab96fc4be4da1657a7a0138')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json
new file mode 100644
index 0000000..72ecdb6
--- /dev/null
+++ b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json
@@ -0,0 +1,483 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "7c3c3ed79c7002102e7af7cfd21c23e0",
+ "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",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "strengthSavingThrowProficiency",
+ "columnName": "strength_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexterityScore",
+ "columnName": "dexterity_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowAdvantage",
+ "columnName": "dexterity_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "dexteritySavingThrowProficiency",
+ "columnName": "dexterity_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionScore",
+ "columnName": "constitution_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowAdvantage",
+ "columnName": "constitution_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "constitutionSavingThrowProficiency",
+ "columnName": "constitution_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceScore",
+ "columnName": "intelligence_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowAdvantage",
+ "columnName": "intelligence_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "intelligenceSavingThrowProficiency",
+ "columnName": "intelligence_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomScore",
+ "columnName": "wisdom_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowAdvantage",
+ "columnName": "wisdom_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "wisdomSavingThrowProficiency",
+ "columnName": "wisdom_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaScore",
+ "columnName": "charisma_score",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "10"
+ },
+ {
+ "fieldPath": "charismaSavingThrowAdvantage",
+ "columnName": "charisma_saving_throw_advantage",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "charismaSavingThrowProficiency",
+ "columnName": "charisma_saving_throw_proficiency",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'none'"
+ },
+ {
+ "fieldPath": "armorType",
+ "columnName": "armor_type",
+ "affinity": "TEXT",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "challengeRating",
+ "columnName": "challenge_rating",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ },
+ {
+ "fieldPath": "customChallengeRatingDescription",
+ "columnName": "custom_challenge_rating_description",
+ "affinity": "TEXT",
+ "notNull": false,
+ "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",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "senses",
+ "columnName": "senses",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "skills",
+ "columnName": "skills",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "damageImmunities",
+ "columnName": "damage_immunities",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "damageResistances",
+ "columnName": "damage_resistances",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "damageVulnerabilities",
+ "columnName": "damage_vulnerabilities",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "conditionImmunities",
+ "columnName": "condition_immunities",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "languages",
+ "columnName": "languages",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "abilities",
+ "columnName": "abilities",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "actions",
+ "columnName": "actions",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "lairActions",
+ "columnName": "lair_actions",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "legendaryActions",
+ "columnName": "legendary_actions",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "regionalActions",
+ "columnName": "regional_actions",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'[]'"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "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": "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "subtype",
+ "columnName": "subtype",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "alignment",
+ "columnName": "alignment",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "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, '7c3c3ed79c7002102e7af7cfd21c23e0')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md
new file mode 100644
index 0000000..24e4113
--- /dev/null
+++ b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md
@@ -0,0 +1,73 @@
+## Monster
+id: UUID as TEXT // doesn't exist in the iOS model
+abilities: Set converted to JSON as TEXT
+actions: Set converted to JSON as TEXT
+alignment: String as TEXT
+armor_type: Enum as TEXT
+blindsight_range: int as INTEGER
+burrow_speed: int as INTEGER
+can_hover: boolean as INTEGER
+challenge_rating: Enum as TEXT
+charisma_saving_throw_advantage
+charisma_saving_throw_proficiency
+charisma_score: int as INTEGER
+climb_speed: int as INTEGER
+condition_immunities: Set converted to JSON as TEXT
+constitution_saving_throw_advantage
+constitution_saving_throw_proficiency
+constitution_score: int as INTEGER
+//other_armor_description: String as TEXT
+custom_challenge_rating_description: String as TEXT
+custom_hit_points_description: String
+custom_proficiency_bonus: int as INTEGER
+custom_speed_description: String as TEXT
+damage_immunities: Set converted to JSON as TEXT
+damage_resistances: Set converted to JSON as TEXT
+damage_vulnerabilities: Set converted to JSON as TEXT
+darkvision_range: int as INTEGER
+dexterity_saving_throw_advantage
+dexterity_saving_throw_proficiency
+dexterity_score: int as INTEGER
+fly_speed: int as INTEGER
+has_custom_hit_points: boolean as INTEGER
+has_custom_speed: boolean as INTEGER
+// has_shield
+hit_dice: int as INTEGER
+intelligence_saving_throw_advantage
+intelligence_saving_throw_proficiency
+intelligence_score: int as INTEGER
+is_blind_beyond_blindsight_range: boolean as INTEGER
+lair_actions
+languages: Set converted to JSON as TEXT
+legendary_actions
+name: String as TEXT
+natural_armor_bonus: int as INTEGER
+other_armor_description: String as TEXT
+reactions
+regional_actions
+// senses
+shield_bonus: int as INTEGER
+size: String as TEXT
+strength_saving_throw_advantage
+strength_saving_throw_proficiency
+strength_score: int as INTEGER
+tag: String as TEXT // subtype || tag
+swim_speed: int as INTEGER
+telepathy_range: int as INTEGER
+tremorsense_range: int as INTEGER
+truesight_range: int as INTEGER
+type: String as TEXT
+understands_but_description: String as TEXT
+walk_speed: int as INTEGER
+wisdom_saving_throw_advantage
+wisdom_saving_throw_proficiency
+wisdom_score: int as INTEGER
+
+// tracked as relationship (don't do this)
+skills: Set converted to JSON as TEXT
+
+## Skill
+// ability_score_name String defaults to "strength"
+// advantage String defaults to "none"
+// name String defaults to ""
+// proficiency String defaults to "none"
diff --git a/Android/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java b/Android/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..c07fd1a
--- /dev/null
+++ b/Android/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.majinnaibu.monstercards;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.majinnaibu.monstercards", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java b/Android/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java
new file mode 100644
index 0000000..ef577f2
--- /dev/null
+++ b/Android/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java
@@ -0,0 +1,22 @@
+package com.majinnaibu.monstercards.init;
+
+import android.app.Application;
+
+import com.majinnaibu.monstercards.BuildConfig;
+import com.microsoft.appcenter.AppCenter;
+import com.microsoft.appcenter.analytics.Analytics;
+import com.microsoft.appcenter.crashes.Crashes;
+
+public class AppCenterInitializer {
+
+ public static void init(Application app) {
+ if (BuildConfig.APPCENTER_SECRET != null && !"".equals(BuildConfig.APPCENTER_SECRET)) {
+ AppCenter.start(
+ app,
+ BuildConfig.APPCENTER_SECRET,
+ Analytics.class,
+ Crashes.class
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java b/Android/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java
new file mode 100644
index 0000000..dd04f40
--- /dev/null
+++ b/Android/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..eaa2137
--- /dev/null
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java b/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java
new file mode 100644
index 0000000..7b27aa6
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java
@@ -0,0 +1,30 @@
+package com.majinnaibu.monstercards;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+import androidx.room.TypeConverters;
+
+import com.majinnaibu.monstercards.data.MonsterDAO;
+import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
+import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
+import com.majinnaibu.monstercards.data.converters.ListOfTraitsConverter;
+import com.majinnaibu.monstercards.data.converters.SetOfLanguageConverter;
+import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter;
+import com.majinnaibu.monstercards.data.converters.SetOfStringConverter;
+import com.majinnaibu.monstercards.data.converters.UUIDConverter;
+import com.majinnaibu.monstercards.models.Monster;
+import com.majinnaibu.monstercards.models.MonsterFTS;
+
+@Database(entities = {Monster.class, MonsterFTS.class}, version = 3)
+@TypeConverters({
+ ArmorTypeConverter.class,
+ ChallengeRatingConverter.class,
+ ListOfTraitsConverter.class,
+ SetOfLanguageConverter.class,
+ SetOfSkillConverter.class,
+ SetOfStringConverter.class,
+ UUIDConverter.class,
+})
+public abstract class AppDatabase extends RoomDatabase {
+ public abstract MonsterDAO monsterDAO();
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
new file mode 100644
index 0000000..2df5ace
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
@@ -0,0 +1,118 @@
+package com.majinnaibu.monstercards;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.navigation.NavController;
+import androidx.navigation.NavDirections;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.majinnaibu.monstercards.helpers.StringHelper;
+import com.majinnaibu.monstercards.init.AppCenterInitializer;
+import com.majinnaibu.monstercards.init.FlipperInitializer;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Objects;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ getOnBackPressedDispatcher().onBackPressed();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppCenterInitializer.init(getApplication());
+ setContentView(R.layout.activity_main);
+ BottomNavigationView navView = findViewById(R.id.nav_view);
+ // Passing each menu ID as a set of Ids because each
+ // menu should be considered as top level destinations.
+ AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
+ R.id.navigation_search,
+ R.id.navigation_dashboard,
+ R.id.navigation_collections,
+ R.id.navigation_library)
+ .build();
+ NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
+ NavController navController = navHostFragment.getNavController();
+ navController.addOnDestinationChangedListener(FlipperInitializer::sendNavigationEvent);
+ NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
+ NavigationUI.setupWithNavController(navView, navController);
+ onNewIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ String json = readMonsterJSONFromIntent(intent);
+ if (!StringHelper.isNullOrEmpty(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);
+ }
+ }
+
+ @Nullable
+ private String readMonsterJSONFromIntent(@NonNull Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ String type = intent.getType();
+ String json;
+ Uri uri = null;
+ 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) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
+ uri = intent.getData();
+ } else {
+ Logger.logError(String.format("unexpected launch configuration action: %s, type: %s", action, type));
+ }
+ if (uri == null) {
+ return null;
+ }
+ json = readContentsOfUri(uri);
+ if (StringHelper.isNullOrEmpty(json)) {
+ return null;
+ }
+ return json;
+ }
+
+ @Nullable
+ private String readContentsOfUri(Uri uri) {
+ StringBuilder builder = new StringBuilder();
+ try (InputStream inputStream =
+ getContentResolver().openInputStream(uri);
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(Objects.requireNonNull(inputStream)))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+ } catch (IOException e) {
+ Logger.logError("error reading file", e);
+ return null;
+ }
+ return builder.toString();
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java
new file mode 100644
index 0000000..575720e
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java
@@ -0,0 +1,82 @@
+package com.majinnaibu.monstercards;
+
+import android.app.Application;
+import android.content.res.Configuration;
+
+import androidx.annotation.NonNull;
+import androidx.room.Room;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import com.majinnaibu.monstercards.data.MonsterRepository;
+import com.majinnaibu.monstercards.init.FlipperInitializer;
+
+public class MonsterCardsApplication extends Application {
+
+ private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ // rename table monster to monsters
+ database.execSQL("ALTER TABLE monster RENAME TO monsters");
+ // create the fts view
+ database.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)");
+ // build the initial full text search index
+ database.execSQL("INSERT INTO monsters_fts(monsters_fts) VALUES('rebuild')");
+
+ }
+ };
+ private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ // Add the senses column
+ database.execSQL("ALTER TABLE monsters ADD COLUMN 'senses' TEXT DEFAULT '[]'");
+ database.execSQL("CREATE TABLE 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 '[]', PRIMARY KEY(`id`))");
+ database.execSQL("INSERT INTO new_monsters(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) 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 FROM monsters");
+ database.execSQL("DROP TABLE monsters");
+ database.execSQL("ALTER TABLE new_monsters RENAME TO monsters");
+ }
+ };
+ private MonsterRepository m_monsterLibraryRepository;
+
+
+ public MonsterCardsApplication() {
+ }
+
+ public MonsterRepository getMonsterRepository() {
+ return m_monsterLibraryRepository;
+ }
+
+ // Called when the application is starting, before any other application objects have been created.
+ // Overriding this method is totally optional!
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // Required initialization logic here!
+
+ FlipperInitializer.init(this);
+
+ // .fallbackToDestructiveMigration()
+ AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters")
+ .addMigrations(MIGRATION_1_2)
+ .addMigrations(MIGRATION_2_3)
+ .fallbackToDestructiveMigrationOnDowngrade()
+// .fallbackToDestructiveMigration()
+ .build();
+ m_monsterLibraryRepository = new MonsterRepository(m_db);
+ }
+
+ // Called by the system when the device configuration changes while your component is running.
+ // Overriding this method is totally optional!
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ }
+
+ // This is called when the overall system is running low on memory,
+ // and would like actively running processes to tighten their belts.
+ // Overriding this method is totally optional!
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java
new file mode 100644
index 0000000..0145afe
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java
@@ -0,0 +1,113 @@
+package com.majinnaibu.monstercards.data;
+
+import androidx.annotation.NonNull;
+
+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;
+
+@SuppressWarnings("unused")
+public final class DevContent {
+ @NonNull
+ public static Monster createSampleMonster() {
+ Monster monster = new Monster();
+ // Name
+ monster.name = "Pixie";
+ // Meta
+ monster.size = "tiny";
+ monster.type = "fey";
+ monster.subtype = "";
+ monster.alignment = "neutral good";
+ monster.armorType = ArmorType.NONE;
+ // Armor & Armor Class
+ monster.shieldBonus = 0;
+ monster.naturalArmorBonus = 7;
+ monster.otherArmorDescription = "14";
+ // Hit Points
+ monster.hitDice = 1;
+ monster.hasCustomHP = false;
+ monster.customHPDescription = "11 (2d8 + 2)";
+ monster.walkSpeed = 10;
+ monster.burrowSpeed = 0;
+ monster.climbSpeed = 0;
+ monster.flySpeed = 30;
+ monster.canHover = false;
+ monster.swimSpeed = 0;
+ monster.hasCustomSpeed = false;
+ monster.customSpeedDescription = "30 ft., swim 30 ft.";
+ // Ability Scores
+ monster.strengthScore = Integer.parseInt("2");
+ monster.dexterityScore = Integer.parseInt("20");
+ monster.constitutionScore = Integer.parseInt("8");
+ monster.intelligenceScore = Integer.parseInt("10");
+ monster.wisdomScore = Integer.parseInt("14");
+ monster.charismaScore = Integer.parseInt("15");
+// monster.strengthScore = 10;
+// monster.dexterityScore = 10;
+// monster.constitutionScore = 10;
+// monster.intelligenceScore = 10;
+// monster.wisdomScore = 10;
+// monster.charismaScore = 10;
+
+ // Saving Throws
+ monster.strengthSavingThrowAdvantage = AdvantageType.NONE;
+ monster.strengthSavingThrowProficiency = ProficiencyType.NONE;
+ monster.dexteritySavingThrowAdvantage = AdvantageType.ADVANTAGE;
+ monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
+ monster.constitutionSavingThrowAdvantage = AdvantageType.DISADVANTAGE;
+ monster.constitutionSavingThrowProficiency = ProficiencyType.EXPERTISE;
+ monster.intelligenceSavingThrowAdvantage = AdvantageType.NONE;
+ monster.intelligenceSavingThrowProficiency = ProficiencyType.EXPERTISE;
+ monster.wisdomSavingThrowAdvantage = AdvantageType.ADVANTAGE;
+ monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
+ monster.charismaSavingThrowAdvantage = AdvantageType.DISADVANTAGE;
+ monster.charismaSavingThrowProficiency = ProficiencyType.NONE;
+ //Skills
+ monster.skills.add(new Skill("perception", AbilityScore.WISDOM));
+ monster.skills.add(new Skill("stealth", AbilityScore.DEXTERITY));
+ // Damage Types
+ monster.damageImmunities.add("force");
+ monster.damageImmunities.add("lightning");
+ monster.damageImmunities.add("poison");
+ monster.damageResistances.add("cold");
+ monster.damageResistances.add("fire");
+ monster.damageResistances.add("piercing");
+ monster.damageVulnerabilities.add("acid");
+ monster.damageVulnerabilities.add("bludgeoning");
+ monster.damageVulnerabilities.add("necrotic");
+ // Condition Immunities
+ monster.conditionImmunities.add("blinded");
+ // Senses
+ monster.senses.add("blindsight 10 ft. (blind beyond this range)");
+ monster.senses.add("darkvision 20 ft.");
+ monster.senses.add("tremorsense 30 ft.");
+ monster.senses.add("truesight 40 ft.");
+ monster.telepathyRange = 20;
+ monster.understandsButDescription = "doesn't care";
+ // Languages
+ monster.languages.add(new Language("English", true));
+ monster.languages.add(new Language("Steve", false));
+ monster.languages.add(new Language("Spanish", true));
+ monster.languages.add(new Language("French", true));
+ monster.languages.add(new Language("Mermataur", false));
+ monster.languages.add(new Language("Goldfish", false));
+ // Challenge Rating
+ monster.challengeRating = ChallengeRating.CUSTOM;
+ monster.customChallengeRatingDescription = "Infinite (0XP)";
+ monster.customProficiencyBonus = 4;
+ // Abilities
+ monster.abilities.add(new Trait("Spellcasting", "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_"));
+ monster.abilities.add(new Trait("Amphibious", "The dragon can breathe air and water."));
+ monster.abilities.add(new Trait("Legendary Resistance (3/Day)", "If the dragon fails a saving throw, it can choose to succeed instead."));
+ // Actions
+ monster.actions.add(new Trait("Club", "_Melee Weapon Attack:_ [STR ATK] to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."));
+
+ return monster;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java
new file mode 100644
index 0000000..9671790
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java
@@ -0,0 +1,39 @@
+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 com.majinnaibu.monstercards.models.Monster;
+
+import java.util.List;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Flowable;
+
+@Dao
+public interface MonsterDAO {
+ @Query("SELECT * FROM monsters")
+ Flowable> getAll();
+
+ @Query("SELECT * FROM monsters WHERE id IN (:monsterIds)")
+ Flowable> loadAllByIds(String[] monsterIds);
+
+ @Query("SELECT * FROM monsters WHERE name LIKE :name LIMIT 1")
+ Flowable findByName(String name);
+
+ @Query("SELECT monsters.* FROM monsters JOIN monsters_fts ON monsters.oid = monsters_fts.docid WHERE monsters_fts MATCH :searchText")
+ Flowable> search(String searchText);
+
+ @Insert
+ Completable insertAll(Monster... monsters);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ Completable save(Monster... monsters);
+
+ @Delete
+ Completable delete(Monster monster);
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java
new file mode 100644
index 0000000..842fb2d
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java
@@ -0,0 +1,112 @@
+package com.majinnaibu.monstercards.data;
+
+import androidx.annotation.NonNull;
+
+import com.majinnaibu.monstercards.AppDatabase;
+import com.majinnaibu.monstercards.helpers.StringHelper;
+import com.majinnaibu.monstercards.models.Monster;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class MonsterRepository {
+
+ private final AppDatabase m_db;
+
+ public MonsterRepository(@NonNull AppDatabase db) {
+ m_db = db;
+ }
+
+ public Flowable> getMonsters() {
+ return m_db.monsterDAO()
+ .getAll()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Flowable> searchMonsters(String searchText) {
+ return m_db.monsterDAO()
+ .getAll()
+ .map(monsters -> {
+ ArrayList filteredMonsters = new ArrayList<>();
+ for (Monster monster : monsters) {
+ if (Helpers.monsterMatchesSearch(monster, searchText)) {
+ filteredMonsters.add(monster);
+ }
+ }
+ return (List) filteredMonsters;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Flowable getMonster(@NonNull UUID monsterId) {
+ return m_db.monsterDAO()
+ .loadAllByIds(new String[]{monsterId.toString()})
+ .map(
+ monsters -> {
+ if (monsters.size() > 0) {
+ return monsters.get(0);
+ } else {
+ return null;
+ }
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Completable addMonster(Monster monster) {
+ Completable result = m_db.monsterDAO().insertAll(monster);
+ result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ return result;
+ }
+
+ public Completable deleteMonster(Monster monster) {
+ Completable result = m_db.monsterDAO().delete(monster);
+ result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ return result;
+ }
+
+ public Completable saveMonster(Monster monster) {
+ Completable result = m_db.monsterDAO().save(monster);
+ result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ return result;
+ }
+
+ private static class Helpers {
+ static boolean monsterMatchesSearch(Monster monster, String searchText) {
+ if (StringHelper.isNullOrEmpty(searchText)) {
+ return true;
+ }
+
+ if (StringHelper.containsCaseInsensitive(monster.name, searchText)) {
+ return true;
+ }
+
+ if (StringHelper.containsCaseInsensitive(monster.size, searchText)) {
+ return true;
+ }
+
+ if (StringHelper.containsCaseInsensitive(monster.type, searchText)) {
+ return true;
+ }
+
+ if (StringHelper.containsCaseInsensitive(monster.subtype, searchText)) {
+ return true;
+ }
+
+ if (StringHelper.containsCaseInsensitive(monster.alignment, searchText)) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java
new file mode 100644
index 0000000..d6d1d59
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java
@@ -0,0 +1,19 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.annotation.NonNull;
+import androidx.room.TypeConverter;
+
+import com.majinnaibu.monstercards.data.enums.ArmorType;
+
+public class ArmorTypeConverter {
+
+ @TypeConverter
+ public static String fromArmorType(@NonNull ArmorType armorType) {
+ return armorType.stringValue;
+ }
+
+ @TypeConverter
+ public static ArmorType armorTypeFromStringValue(String stringValue) {
+ return ArmorType.valueOfString(stringValue);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java
new file mode 100644
index 0000000..3d9712c
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java
@@ -0,0 +1,19 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.annotation.NonNull;
+import androidx.room.TypeConverter;
+
+import com.majinnaibu.monstercards.data.enums.ChallengeRating;
+
+public class ChallengeRatingConverter {
+
+ @TypeConverter
+ public static String fromChallengeRating(@NonNull ChallengeRating challengeRating) {
+ return challengeRating.stringValue;
+ }
+
+ @TypeConverter
+ public static ChallengeRating challengeRatingFromStringValue(String stringValue) {
+ return ChallengeRating.valueOfString(stringValue);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java
new file mode 100644
index 0000000..7a0e2ef
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.room.TypeConverter;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.majinnaibu.monstercards.models.Trait;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ListOfTraitsConverter {
+ @TypeConverter
+ public static String fromListOfTraits(List traits) {
+ Gson gson = new Gson();
+ return gson.toJson(traits);
+ }
+
+ @TypeConverter
+ public static List listOfTraitsFromString(String string) {
+ Gson gson = new Gson();
+ Type setType = new TypeToken>() {
+ }.getType();
+ return gson.fromJson(string, setType);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java
new file mode 100644
index 0000000..b076a4d
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java
@@ -0,0 +1,28 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.room.TypeConverter;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.majinnaibu.monstercards.models.Language;
+
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SetOfLanguageConverter {
+
+ @TypeConverter
+ public static String fromSetOfLanguage(Set languages) {
+ Gson gson = new Gson();
+ return gson.toJson(languages);
+ }
+
+ @TypeConverter
+ public static Set setOfLanguageFromString(String string) {
+ Gson gson = new Gson();
+ Type setType = new TypeToken>() {
+ }.getType();
+ return gson.fromJson(string, setType);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java
new file mode 100644
index 0000000..6f2ee90
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java
@@ -0,0 +1,28 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.room.TypeConverter;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.majinnaibu.monstercards.models.Skill;
+
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SetOfSkillConverter {
+
+ @TypeConverter
+ public static String fromSetOfSkill(Set skills) {
+ Gson gson = new Gson();
+ return gson.toJson(skills);
+ }
+
+ @TypeConverter
+ public static Set setOfSkillFromString(String string) {
+ Gson gson = new Gson();
+ Type setType = new TypeToken>() {
+ }.getType();
+ return gson.fromJson(string, setType);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java
new file mode 100644
index 0000000..c09dcaa
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.room.TypeConverter;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SetOfStringConverter {
+
+ @TypeConverter
+ public static String fromSetOfString(Set strings) {
+ Gson gson = new Gson();
+ return gson.toJson(strings);
+ }
+
+ @TypeConverter
+ public static Set setOfStringFromString(String string) {
+ Gson gson = new Gson();
+ Type setType = new TypeToken>() {
+ }.getType();
+ return gson.fromJson(string, setType);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java
new file mode 100644
index 0000000..d775179
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java
@@ -0,0 +1,20 @@
+package com.majinnaibu.monstercards.data.converters;
+
+import androidx.annotation.NonNull;
+import androidx.room.TypeConverter;
+
+import java.util.UUID;
+
+public class UUIDConverter {
+
+ @NonNull
+ @TypeConverter
+ public static String fromUUID(@NonNull UUID uuid) {
+ return uuid.toString();
+ }
+
+ @TypeConverter
+ public static UUID uuidFromString(String string) {
+ return UUID.fromString(string);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java
new file mode 100644
index 0000000..5a0b24e
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java
@@ -0,0 +1,31 @@
+package com.majinnaibu.monstercards.data.enums;
+
+@SuppressWarnings("unused")
+public enum AbilityScore {
+ STRENGTH("strength", "Strength", "STR"),
+ DEXTERITY("dexterity", "Dexterity", "DEX"),
+ CONSTITUTION("constitution", "Constitution", "CON"),
+ INTELLIGENCE("intelligence", "Intelligence", "INT"),
+ WISDOM("wisdom", "Wisdom", "WIS"),
+ CHARISMA("charisma", "Charisma", "CHA"),
+ ;
+
+ public final String displayName;
+ public final String shortDisplayName;
+ public final String stringValue;
+
+ AbilityScore(String stringValue, String displayName, String shortDisplayName) {
+ this.displayName = displayName;
+ this.stringValue = stringValue;
+ this.shortDisplayName = shortDisplayName;
+ }
+
+ public static AbilityScore valueOfString(String string) {
+ for (AbilityScore abilityScore : values()) {
+ if (abilityScore.stringValue.equals(string)) {
+ return abilityScore;
+ }
+ }
+ return AbilityScore.STRENGTH;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java
new file mode 100644
index 0000000..1405696
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.data.enums;
+
+public enum AdvantageType {
+ NONE("none", "None", ""),
+ ADVANTAGE("advantage", "Advantage", "A"),
+ DISADVANTAGE("disadvantage", "Disadvantage", "D"),
+ ;
+
+ public final String displayName;
+ public final String stringValue;
+ public final String label;
+
+ AdvantageType(String stringValue, String displayName, String label) {
+ this.displayName = displayName;
+ this.stringValue = stringValue;
+ this.label = label;
+ }
+
+ public static AdvantageType valueOfString(String string) {
+ for (AdvantageType advantageType : values()) {
+ if (advantageType.stringValue.equals(string)) {
+ return advantageType;
+ }
+ }
+ return AdvantageType.NONE;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java
new file mode 100644
index 0000000..67cf8ae
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java
@@ -0,0 +1,41 @@
+package com.majinnaibu.monstercards.data.enums;
+
+@SuppressWarnings("unused")
+public enum ArmorType {
+ NONE("none", "None", 10),
+ NATURAL_ARMOR("natural armor", "Natural Armor", 10),
+ MAGE_ARMOR("mage armor", "Mage Armor", 10),
+ PADDED("padded", "Padded", 11),
+ LEATHER("leather", "Leather", 11),
+ STUDDED_LEATHER("studded", "Studded Leather", 12),
+ HIDE("hide", "Hide", 12),
+ CHAIN_SHIRT("chain shirt", "Chain Shirt", 13),
+ SCALE_MAIL("scale mail", "Scale Mail", 14),
+ BREASTPLATE("breastplate", "Breastplate", 14),
+ HALF_PLATE("half plate", "Half Plate", 15),
+ RING_MAIL("ring mail", "Ring Mail", 14),
+ CHAIN_MAIL("chain mail", "Chain Mail", 16),
+ SPLINT_MAIL("splint", "Splint Mail", 17),
+ PLATE_MAIL("plate", "Plate Mail", 18),
+ OTHER("other", "Other", 10),
+ ;
+
+ public final String displayName;
+ public final String stringValue;
+ public final int baseArmorClass;
+
+ ArmorType(String stringValue, String displayName, int baseArmorClass) {
+ this.displayName = displayName;
+ this.stringValue = stringValue;
+ this.baseArmorClass = baseArmorClass;
+ }
+
+ public static ArmorType valueOfString(String string) {
+ for (ArmorType armorType : values()) {
+ if (armorType.stringValue.equals(string)) {
+ return armorType;
+ }
+ }
+ return ArmorType.NONE;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java
new file mode 100644
index 0000000..be8d87c
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java
@@ -0,0 +1,60 @@
+package com.majinnaibu.monstercards.data.enums;
+
+@SuppressWarnings("unused")
+public enum ChallengeRating {
+ CUSTOM("custom", "Custom", 0),
+ ZERO("zero", "0 (10 XP)", 2),
+ ONE_EIGHTH("1/8", "1/8 (25 XP)", 2),
+ ONE_QUARTER("1/4", "1/4 (50 XP)", 2),
+ ONE_HALF("1/2", "1/2 (100 XP)", 2),
+ ONE("1", "1 (200 XP)", 2),
+ TWO("2", "2 (450 XP)", 2),
+ THREE("3", "3 (700 XP)", 2),
+ FOUR("4", "4 (1,100 XP)", 2),
+ FIVE("5", "5 (1,800 XP)", 3),
+ SIX("6", "6 (2,300 XP)", 3),
+ SEVEN("7", "7 (2,900 XP)", 3),
+ EIGHT("8", "8 (3,900 XP)", 3),
+ NINE("9", "9 (5,000 XP)", 4),
+ TEN("10", "10 (5,900 XP)", 4),
+ ELEVEN("11", "11 (7,200 XP)", 4),
+ TWELVE("12", "12 (8,400 XP)", 4),
+ THIRTEEN("13", "13 (10,000 XP)", 5),
+ FOURTEEN("14", "14 (11,500 XP)", 5),
+ FIFTEEN("15", "15 (13,000 XP)", 5),
+ SIXTEEN("16", "16 (15,000 XP)", 5),
+ SEVENTEEN("17", "17 (18,000 XP)", 6),
+ EIGHTEEN("18", "18 (20,000 XP)", 6),
+ NINETEEN("19", "19 (22,000 XP)", 6),
+ TWENTY("20", "20 (25,000 XP)", 6),
+ TWENTY_ONE("21", "21 (33,000 XP)", 7),
+ TWENTY_TWO("22", "22 (41,000 XP)", 7),
+ TWENTY_THREE("23", "23 (50,000 XP)", 7),
+ TWENTY_FOUR("24", "24 (62,000 XP)", 7),
+ TWENTY_FIVE("25", "25 (75,000 XP)", 8),
+ TWENTY_SIX("26", "26 (90,000 XP)", 8),
+ TWENTY_SEVEN("27", "27 (105,000 XP)", 8),
+ TWENTY_EIGHT("28", "28 (120,000 XP)", 8),
+ TWENTY_NINE("29", "29 (135,000 XP)", 9),
+ THIRTY("30", "30 (155,000 XP)", 9),
+ ;
+
+ public final String displayName;
+ public final String stringValue;
+ public final int proficiencyBonus;
+
+ ChallengeRating(String stringValue, String displayName, int proficiencyBonus) {
+ this.displayName = displayName;
+ this.stringValue = stringValue;
+ this.proficiencyBonus = proficiencyBonus;
+ }
+
+ public static ChallengeRating valueOfString(String string) {
+ for (ChallengeRating challengeRating : values()) {
+ if (challengeRating.stringValue.equals(string)) {
+ return challengeRating;
+ }
+ }
+ return ChallengeRating.ONE;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java
new file mode 100644
index 0000000..9236ad0
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.data.enums;
+
+public enum ProficiencyType {
+ NONE("none", "None", ""),
+ PROFICIENT("proficient", "Proficient", "P"),
+ EXPERTISE("expertise", "Expertise", "Ex"),
+ ;
+
+ public final String displayName;
+ public final String stringValue;
+ public final String label;
+
+ ProficiencyType(String stringValue, String displayName, String label) {
+ this.displayName = displayName;
+ this.stringValue = stringValue;
+ this.label = label;
+ }
+
+ public static ProficiencyType valueOfString(String string) {
+ for (ProficiencyType proficiencyType : values()) {
+ if (proficiencyType.stringValue.equals(string)) {
+ return proficiencyType;
+ }
+ }
+ return ProficiencyType.NONE;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java
new file mode 100644
index 0000000..67b7497
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java
@@ -0,0 +1,9 @@
+package com.majinnaibu.monstercards.data.enums;
+
+public enum StringType {
+ CONDITION_IMMUNITY,
+ DAMAGE_IMMUNITY,
+ DAMAGE_RESISTANCE,
+ DAMAGE_VULNERABILITY,
+ SENSE
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java
new file mode 100644
index 0000000..ab7e858
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java
@@ -0,0 +1,10 @@
+package com.majinnaibu.monstercards.data.enums;
+
+public enum TraitType {
+ ABILITY,
+ ACTION,
+ LAIR_ACTION,
+ LEGENDARY_ACTION,
+ REGIONAL_ACTION,
+ REACTIONS
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java
new file mode 100644
index 0000000..2a5d044
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java
@@ -0,0 +1,17 @@
+package com.majinnaibu.monstercards.helpers;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+public final class ArrayHelper {
+ public static int indexOf(@NonNull Object[] array, Object target) {
+ for (int index = 0; index < array.length; index++) {
+ if (Objects.equals(array[index], target)) {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java
new file mode 100644
index 0000000..bf1e931
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.helpers;
+
+import org.commonmark.node.Document;
+import org.commonmark.node.Node;
+import org.commonmark.node.Paragraph;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+
+public final class CommonMarkHelper {
+ public static String toHtml(String rawCommonMark) {
+ Parser parser = Parser.builder().build();
+ Node document = parser.parse(rawCommonMark);
+ Node parent1 = document.getFirstChild();
+ Node parent2 = document.getLastChild();
+ if (parent1 == parent2 && parent1 instanceof Paragraph) {
+ document = new Document();
+ Node child = parent1.getFirstChild();
+ while (child != null) {
+ Node nextChild = child.getNext();
+ document.appendChild(child);
+ child = nextChild;//child.getNext();
+ }
+ }
+ HtmlRenderer renderer = HtmlRenderer.builder().build();
+ return renderer.render(document);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java
new file mode 100644
index 0000000..de7e0e8
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java
@@ -0,0 +1,311 @@
+package com.majinnaibu.monstercards.helpers;
+
+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 MonsterImportHelper {
+ @NonNull
+ public static Monster fromJSON(String json) {
+ JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject();
+
+ Monster monster = new Monster();
+ monster.name = Helpers.getString(rootDict, "name");
+ monster.size = Helpers.getString(rootDict, "size");
+ monster.type = Helpers.getString(rootDict, "type");
+ 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 getListOfTraits(@NonNull JsonObject dict, String name) {
+ ArrayList 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;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @NonNull
+ public static Set getSetOfSkills(@NonNull JsonObject root) {
+ HashSet 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 getSetOfDamageTypes(JsonObject rootDict, String name) {
+ return getSetOfDamageTypes(rootDict, name, null);
+ }
+
+ @NonNull
+ public static Set getSetOfDamageTypes(@NonNull JsonObject root, String name, String type) {
+ HashSet 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 getSetOfLanguages(@NonNull JsonObject root, String name) {
+ HashSet 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;
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java
new file mode 100644
index 0000000..bdad3b2
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java
@@ -0,0 +1,79 @@
+package com.majinnaibu.monstercards.helpers;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Collection;
+
+@SuppressWarnings({"RedundantIfStatement"})
+public final class StringHelper {
+ public static boolean isNullOrEmpty(CharSequence value) {
+ if (value == null) {
+ return true;
+ }
+
+ if ("".contentEquals(value)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @NonNull
+ public static String join(String delimiter, @NonNull Collection strings) {
+ int length = strings.size();
+ if (length < 1) {
+ return "";
+ } else {
+ StringBuilder sb = new StringBuilder();
+ boolean isFirst = true;
+ for (String element : strings) {
+ if (!isFirst) {
+ sb.append(delimiter);
+ }
+
+ sb.append(element);
+
+ isFirst = false;
+ }
+ return sb.toString();
+ }
+ }
+
+ public static String oxfordJoin(String delimiter, String lastDelimiter, String onlyDelimiter, @NonNull Collection strings) {
+ int length = strings.size();
+ if (length < 1) {
+ return "";
+ } else if (length == 2) {
+ return join(onlyDelimiter, strings);
+ } else {
+ StringBuilder sb = new StringBuilder();
+ int index = 0;
+ int lastIndex = length - 1;
+ for (String element : strings) {
+ if (index > 0 && index < lastIndex) {
+ sb.append(delimiter);
+ } else if (index > 0 && index >= lastIndex) {
+ sb.append(lastDelimiter);
+ }
+ sb.append(element);
+ index++;
+ }
+ return sb.toString();
+ }
+ }
+
+ @Nullable
+ public static Integer parseInt(String s) {
+ try {
+ return Integer.parseInt(s);
+ } catch (NumberFormatException _ex) {
+ return null;
+ }
+ }
+
+ public static boolean containsCaseInsensitive(@NonNull String text, @NonNull String search) {
+ // TODO: find a locale independent way to do this
+ return text.toLowerCase().contains(search.toLowerCase());
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Language.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Language.java
new file mode 100644
index 0000000..a670740
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Language.java
@@ -0,0 +1,74 @@
+package com.majinnaibu.monstercards.models;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+public class Language implements Comparator, Comparable {
+
+ private String mName;
+ private boolean mSpeaks;
+
+ public Language(String name, boolean speaks) {
+ mName = name;
+ mSpeaks = speaks;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String value) {
+ mName = value;
+ }
+
+ public boolean getSpeaks() {
+ return mSpeaks;
+ }
+
+ public void setSpeaks(boolean value) {
+ mSpeaks = value;
+ }
+
+ @Override
+ public int compareTo(Language o) {
+ if (this.mSpeaks && !o.mSpeaks) {
+ return -1;
+ }
+ if (!this.mSpeaks && o.mSpeaks) {
+ return 1;
+ }
+ return this.mName.compareToIgnoreCase(o.mName);
+ }
+
+ @Override
+ public int compare(@NonNull Language o1, Language o2) {
+ if (o1.mSpeaks && !o2.mSpeaks) {
+ return -1;
+ }
+ if (!o1.mSpeaks && o2.mSpeaks) {
+ return 1;
+ }
+ return o1.mName.compareToIgnoreCase(o2.mName);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof Language)) {
+ return false;
+ }
+ Language otherLanguage = (Language) obj;
+ if (!Objects.equals(this.mName, otherLanguage.mName)) {
+ return false;
+ }
+ if (this.mSpeaks != otherLanguage.mSpeaks) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
new file mode 100644
index 0000000..aa04826
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
@@ -0,0 +1,1070 @@
+package com.majinnaibu.monstercards.models;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+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.helpers.StringHelper;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+@Entity(tableName = "monsters")
+@SuppressLint("DefaultLocale")
+@SuppressWarnings("unused")
+public class Monster {
+
+ @PrimaryKey
+ @NonNull
+ public UUID id;
+
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ public String name;
+
+ @NonNull()
+ @ColumnInfo(defaultValue = "")
+ public String size;
+
+ @NonNull()
+ @ColumnInfo(defaultValue = "")
+ public String type;
+
+ @NonNull()
+ @ColumnInfo(defaultValue = "")
+ public String subtype;
+
+ @NonNull()
+ @ColumnInfo(defaultValue = "")
+ public String alignment;
+
+ @ColumnInfo(name = "strength_score", defaultValue = "10")
+ public int strengthScore;
+
+ @ColumnInfo(name = "strength_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType strengthSavingThrowAdvantage;
+
+ @ColumnInfo(name = "strength_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType strengthSavingThrowProficiency;
+
+ @ColumnInfo(name = "dexterity_score", defaultValue = "10")
+ public int dexterityScore;
+
+ @ColumnInfo(name = "dexterity_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType dexteritySavingThrowAdvantage;
+
+ @ColumnInfo(name = "dexterity_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType dexteritySavingThrowProficiency;
+
+ @ColumnInfo(name = "constitution_score", defaultValue = "10")
+ public int constitutionScore;
+
+ @ColumnInfo(name = "constitution_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType constitutionSavingThrowAdvantage;
+
+ @ColumnInfo(name = "constitution_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType constitutionSavingThrowProficiency;
+
+ @ColumnInfo(name = "intelligence_score", defaultValue = "10")
+ public int intelligenceScore;
+
+ @ColumnInfo(name = "intelligence_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType intelligenceSavingThrowAdvantage;
+
+ @ColumnInfo(name = "intelligence_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType intelligenceSavingThrowProficiency;
+
+ @ColumnInfo(name = "wisdom_score", defaultValue = "10")
+ public int wisdomScore;
+
+ @ColumnInfo(name = "wisdom_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType wisdomSavingThrowAdvantage;
+
+ @ColumnInfo(name = "wisdom_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType wisdomSavingThrowProficiency;
+
+ @ColumnInfo(name = "charisma_score", defaultValue = "10")
+ public int charismaScore;
+
+ @ColumnInfo(name = "charisma_saving_throw_advantage", defaultValue = "none")
+ public AdvantageType charismaSavingThrowAdvantage;
+
+ @ColumnInfo(name = "charisma_saving_throw_proficiency", defaultValue = "none")
+ public ProficiencyType charismaSavingThrowProficiency;
+
+ @ColumnInfo(name = "armor_type", defaultValue = "none"/* ArmorType.NONE.stringValue */)
+ public ArmorType armorType;
+
+ @ColumnInfo(name = "shield_bonus", defaultValue = "0")
+ public int shieldBonus;
+
+ @ColumnInfo(name = "natural_armor_bonus", defaultValue = "0")
+ public int naturalArmorBonus;
+
+ @ColumnInfo(name = "other_armor_description", defaultValue = "")
+ public String otherArmorDescription;
+
+ @ColumnInfo(name = "hit_dice", defaultValue = "1")
+ public int hitDice;
+
+ @ColumnInfo(name = "has_custom_hit_points", defaultValue = "")
+ public boolean hasCustomHP;
+
+ @ColumnInfo(name = "custom_hit_points_description", defaultValue = "")
+ public String customHPDescription;
+
+ @ColumnInfo(name = "walk_speed", defaultValue = "0")
+ public int walkSpeed;
+
+ @ColumnInfo(name = "burrow_speed", defaultValue = "0")
+ public int burrowSpeed;
+
+ @ColumnInfo(name = "climb_speed", defaultValue = "0")
+ public int climbSpeed;
+
+ @ColumnInfo(name = "fly_speed", defaultValue = "0")
+ public int flySpeed;
+
+ @ColumnInfo(name = "can_hover", defaultValue = "false")
+ public boolean canHover;
+
+ @ColumnInfo(name = "swim_speed", defaultValue = "0")
+ public int swimSpeed;
+
+ @ColumnInfo(name = "has_custom_speed", defaultValue = "false")
+ public boolean hasCustomSpeed;
+
+ @ColumnInfo(name = "custom_speed_description")
+ public String customSpeedDescription;
+
+ @ColumnInfo(name = "challenge_rating", defaultValue = "1")
+ public ChallengeRating challengeRating;
+
+ @ColumnInfo(name = "custom_challenge_rating_description", defaultValue = "")
+ public String customChallengeRatingDescription;
+
+ @ColumnInfo(name = "custom_proficiency_bonus", defaultValue = "0")
+ public int customProficiencyBonus;
+
+ @ColumnInfo(name = "telepathy_range", defaultValue = "0")
+ public int telepathyRange;
+
+ @ColumnInfo(name = "understands_but_description", defaultValue = "")
+ public String understandsButDescription;
+
+ @ColumnInfo(name = "senses", defaultValue = "[]")
+ public Set senses;
+
+ @ColumnInfo(name = "skills", defaultValue = "[]")
+ public Set skills;
+
+ @ColumnInfo(name = "damage_immunities", defaultValue = "[]")
+ public Set damageImmunities;
+
+ @ColumnInfo(name = "damage_resistances", defaultValue = "[]")
+ public Set damageResistances;
+
+ @ColumnInfo(name = "damage_vulnerabilities", defaultValue = "[]")
+ public Set damageVulnerabilities;
+
+ @ColumnInfo(name = "condition_immunities", defaultValue = "[]")
+ public Set conditionImmunities;
+
+ @ColumnInfo(name = "languages", defaultValue = "[]")
+ public Set languages;
+
+ @ColumnInfo(name = "abilities", defaultValue = "[]")
+ public List abilities;
+
+ @ColumnInfo(name = "actions", defaultValue = "[]")
+ public List actions;
+
+ @ColumnInfo(name = "reactions", defaultValue = "[]")
+ public List reactions;
+
+ @ColumnInfo(name = "lair_actions", defaultValue = "[]")
+ public List lairActions;
+
+ @ColumnInfo(name = "legendary_actions", defaultValue = "[]")
+ public List legendaryActions;
+
+ @ColumnInfo(name = "regional_actions", defaultValue = "[]")
+ public List regionalActions;
+
+ public Monster() {
+ id = UUID.randomUUID();
+ name = "";
+ size = "";
+ type = "";
+ subtype = "";
+ alignment = "";
+ strengthScore = 10;
+ dexterityScore = 10;
+ constitutionScore = 10;
+ intelligenceScore = 10;
+ wisdomScore = 10;
+ charismaScore = 10;
+ armorType = ArmorType.NONE;
+ shieldBonus = 0;
+ naturalArmorBonus = 0;
+ otherArmorDescription = "";
+ hitDice = 1;
+ hasCustomHP = false;
+ customHPDescription = "";
+ walkSpeed = 0;
+ burrowSpeed = 0;
+ climbSpeed = 0;
+ flySpeed = 0;
+ canHover = false;
+ swimSpeed = 0;
+ hasCustomSpeed = false;
+ customSpeedDescription = "";
+ challengeRating = ChallengeRating.ONE;
+ customChallengeRatingDescription = "";
+ customProficiencyBonus = 0;
+ telepathyRange = 0;
+ understandsButDescription = "";
+ strengthSavingThrowAdvantage = AdvantageType.NONE;
+ strengthSavingThrowProficiency = ProficiencyType.NONE;
+ dexteritySavingThrowAdvantage = AdvantageType.NONE;
+ dexteritySavingThrowProficiency = ProficiencyType.NONE;
+ constitutionSavingThrowAdvantage = AdvantageType.NONE;
+ constitutionSavingThrowProficiency = ProficiencyType.NONE;
+ intelligenceSavingThrowAdvantage = AdvantageType.NONE;
+ intelligenceSavingThrowProficiency = ProficiencyType.NONE;
+ wisdomSavingThrowAdvantage = AdvantageType.NONE;
+ wisdomSavingThrowProficiency = ProficiencyType.NONE;
+ charismaSavingThrowAdvantage = AdvantageType.NONE;
+ charismaSavingThrowProficiency = ProficiencyType.NONE;
+
+
+ skills = new HashSet<>();
+ senses = new HashSet<>();
+ damageImmunities = new HashSet<>();
+ damageResistances = new HashSet<>();
+ damageVulnerabilities = new HashSet<>();
+ conditionImmunities = new HashSet<>();
+ languages = new HashSet<>();
+ abilities = new ArrayList<>();
+ actions = new ArrayList<>();
+ reactions = new ArrayList<>();
+ lairActions = new ArrayList<>();
+ legendaryActions = new ArrayList<>();
+ regionalActions = new ArrayList<>();
+ }
+
+ public String getMeta() {
+ StringBuilder sb = new StringBuilder();
+ boolean isFirstOutput = true;
+ if (!StringHelper.isNullOrEmpty(size)) {
+ sb.append(size);
+ isFirstOutput = false;
+ }
+
+ if (!StringHelper.isNullOrEmpty(type)) {
+ if (!isFirstOutput) {
+ sb.append(" ");
+ }
+ sb.append(type);
+ isFirstOutput = false;
+ }
+
+ if (!StringHelper.isNullOrEmpty(subtype)) {
+ if (!isFirstOutput) {
+ sb.append(" ");
+ }
+ sb.append("(");
+ sb.append(subtype);
+ sb.append(")");
+ isFirstOutput = false;
+ }
+
+ if (!StringHelper.isNullOrEmpty(alignment)) {
+ if (!isFirstOutput) {
+ sb.append(", ");
+ }
+ sb.append(alignment);
+ }
+
+ return sb.toString();
+ }
+
+ public int getAbilityScore(@NonNull AbilityScore abilityScore) {
+ switch (abilityScore) {
+ case STRENGTH:
+ return strengthScore;
+ case DEXTERITY:
+ return dexterityScore;
+ case CONSTITUTION:
+ return constitutionScore;
+ case INTELLIGENCE:
+ return intelligenceScore;
+ case WISDOM:
+ return wisdomScore;
+ case CHARISMA:
+ return charismaScore;
+ default:
+ return 10;
+ }
+ }
+
+ public int getAbilityModifier(@NonNull AbilityScore abilityScore) {
+ switch (abilityScore) {
+ case STRENGTH:
+ return getStrengthModifier();
+ case DEXTERITY:
+ return getDexterityModifier();
+ case CONSTITUTION:
+ return getConstitutionModifier();
+ case INTELLIGENCE:
+ return getIntelligenceModifier();
+ case WISDOM:
+ return getWisdomModifier();
+ case CHARISMA:
+ return getCharismaModifier();
+ default:
+ return 0;
+ }
+ }
+
+ public AdvantageType getSavingThrowAdvantageType(@NonNull AbilityScore abilityScore) {
+ switch (abilityScore) {
+ case STRENGTH:
+ return strengthSavingThrowAdvantage;
+ case DEXTERITY:
+ return dexteritySavingThrowAdvantage;
+ case CONSTITUTION:
+ return constitutionSavingThrowAdvantage;
+ case INTELLIGENCE:
+ return intelligenceSavingThrowAdvantage;
+ case WISDOM:
+ return wisdomSavingThrowAdvantage;
+ case CHARISMA:
+ return charismaSavingThrowAdvantage;
+ default:
+ return AdvantageType.NONE;
+ }
+ }
+
+ public ProficiencyType getSavingThrowProficiencyType(@NonNull AbilityScore abilityScore) {
+ switch (abilityScore) {
+ case STRENGTH:
+ return strengthSavingThrowProficiency;
+ case DEXTERITY:
+ return dexteritySavingThrowProficiency;
+ case CONSTITUTION:
+ return constitutionSavingThrowProficiency;
+ case INTELLIGENCE:
+ return intelligenceSavingThrowProficiency;
+ case WISDOM:
+ return wisdomSavingThrowProficiency;
+ case CHARISMA:
+ return charismaSavingThrowProficiency;
+ default:
+ return ProficiencyType.NONE;
+ }
+ }
+
+ public int getStrengthModifier() {
+ return Helpers.getAbilityModifierForScore(strengthScore);
+ }
+
+ public int getDexterityModifier() {
+ return Helpers.getAbilityModifierForScore(dexterityScore);
+ }
+
+ public int getConstitutionModifier() {
+ return Helpers.getAbilityModifierForScore(constitutionScore);
+ }
+
+ public int getIntelligenceModifier() {
+ return Helpers.getAbilityModifierForScore(intelligenceScore);
+ }
+
+ public int getWisdomModifier() {
+ return Helpers.getAbilityModifierForScore(wisdomScore);
+ }
+
+ public int getCharismaModifier() {
+ return Helpers.getAbilityModifierForScore(charismaScore);
+ }
+
+ public String getArmorClass() {
+ boolean hasShield = shieldBonus != 0;
+ ArmorType armorType = this.armorType != null ? this.armorType : ArmorType.NONE;
+ switch (armorType) {
+ case NONE:
+ // 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)"
+ return String.format("%d%s", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? " (shield)" : "");
+ case NATURAL_ARMOR:
+ // 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)"
+ return String.format("%d (natural armor%s)", armorType.baseArmorClass + getDexterityModifier() + naturalArmorBonus + shieldBonus, hasShield ? ", shield" : "");
+ case MAGE_ARMOR:
+ // 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor)
+ return String.format("%d (%s%d with mage armor)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? "shield, " : "", armorType.baseArmorClass + 3 + getDexterityModifier() + shieldBonus);
+ case PADDED:
+ // 11 + dexMod + 2 for shield "18 (padded armor, shield)"
+ return String.format("%d (padded%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : "");
+ case LEATHER:
+ // 11 + dexMod + 2 for shield "18 (leather, shield)"
+ return String.format("%d (leather%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : "");
+ case STUDDED_LEATHER:
+ // 12 + dexMod +2 for shield "17 (studded leather)"
+ return String.format("%d (studded leather%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : "");
+ case HIDE:
+ // 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)"
+ return String.format("%d (hide%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : "");
+ case CHAIN_SHIRT:
+ // 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)"
+ return String.format("%d (chain shirt%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : "");
+ case SCALE_MAIL:
+ // 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)"
+ return String.format("%d (scale mail%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : "");
+ case BREASTPLATE:
+ // 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)"
+ return String.format("%d (breastplate%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : "");
+ case HALF_PLATE:
+ // 15 + Min(2, dexMod) + 2 for shield "17 (half plate)"
+ return String.format("%d (half plate%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : "");
+ case RING_MAIL:
+ // 14 + 2 for shield "14 (ring mail)
+ return String.format("%d (ring mail%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : "");
+ case CHAIN_MAIL:
+ // 16 + 2 for shield "16 (chain mail)"
+ return String.format("%d (chain mail%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : "");
+ case SPLINT_MAIL:
+ // 17 + 2 for shield "17 (splint)"
+ return String.format("%d (splint%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : "");
+ case PLATE_MAIL:
+ // 18 + 2 for shield "18 (plate)"
+ return String.format("%d (plate%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : "");
+ case OTHER:
+ // pure string value shield check does nothing just copies the string from otherArmorDesc
+ return otherArmorDescription;
+ default:
+ return "";
+ }
+ }
+
+ public int getArmorClassValue() {
+ boolean hasShield = shieldBonus != 0;
+ ArmorType armorType = this.armorType != null ? this.armorType : ArmorType.NONE;
+ switch (armorType) {
+ case NATURAL_ARMOR:
+ // 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)"
+ return armorType.baseArmorClass + getDexterityModifier() + naturalArmorBonus + shieldBonus;
+ case MAGE_ARMOR:
+ // 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor)
+ return armorType.baseArmorClass + 3 + getDexterityModifier() + shieldBonus;
+ case NONE:
+ // 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)"
+ case PADDED:
+ // 11 + dexMod + 2 for shield "18 (padded armor, shield)"
+ case LEATHER:
+ // 11 + dexMod + 2 for shield "18 (leather, shield)"
+ case STUDDED_LEATHER:
+ // 12 + dexMod +2 for shield "17 (studded leather)"
+ return armorType.baseArmorClass + getDexterityModifier() + shieldBonus;
+ case HIDE:
+ // 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)"
+ case CHAIN_SHIRT:
+ // 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)"
+ case SCALE_MAIL:
+ // 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)"
+ case BREASTPLATE:
+ // 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)"
+ case HALF_PLATE:
+ // 15 + Min(2, dexMod) + 2 for shield "17 (half plate)"
+ return armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus;
+ case RING_MAIL:
+ // 14 + 2 for shield "14 (ring mail)
+ case CHAIN_MAIL:
+ // 16 + 2 for shield "16 (chain mail)"
+ case SPLINT_MAIL:
+ // 17 + 2 for shield "17 (splint)"
+ case PLATE_MAIL:
+ // 18 + 2 for shield "18 (plate)"
+ return armorType.baseArmorClass + shieldBonus;
+ case OTHER:
+ // pure string value shield check does nothing just copies the string from otherArmorDesc
+ return 0;
+ default:
+ Logger.logUnimplementedFeature(String.format("Getting the armor class value with an unknown armor type %s", armorType));
+ return -1;
+ }
+ }
+
+ public String getHitPoints() {
+ if (hasCustomHP) {
+ return customHPDescription;
+ } else {
+ int dieSize = Helpers.getHitDieForSize(size);
+ int conMod = getConstitutionModifier();
+ // For PC style calculations use this
+ //int hpTotal = (int) Math.max(1, Math.ceil(dieSize + conMod + (hitDice - 1) * ((dieSize + 1) / 2.0 + conMod)));
+ // For monster style calculations use this
+ int hpTotal = (int) Math.max(1, Math.ceil(hitDice * ((dieSize + 1) / 2.0 + conMod)));
+ return String.format("%d (%dd%d %+d)", hpTotal, hitDice, dieSize, conMod * hitDice);
+ }
+ }
+
+ public int getHitPointsValue() {
+ if (hasCustomHP) {
+ return 0;
+ } else {
+ int dieSize = Helpers.getHitDieForSize(size);
+ int conMod = getConstitutionModifier();
+ // For PC style calculations use this
+ //return (int) Math.max(1, Math.ceil(dieSize + conMod + (hitDice - 1) * ((dieSize + 1) / 2.0 + conMod)));
+ // For monster style calculations use this
+ return (int) Math.max(1, Math.ceil(hitDice * ((dieSize + 1) / 2.0 + conMod)));
+ }
+ }
+
+ public String getSpeedText() {
+ if (hasCustomSpeed) {
+ return customSpeedDescription;
+ } else {
+ ArrayList speedParts = new ArrayList<>();
+ if (walkSpeed > 0) {
+ speedParts.add(String.format("%d ft.", walkSpeed));
+ }
+ if (burrowSpeed > 0) {
+ speedParts.add(String.format("burrow %d ft.", burrowSpeed));
+ }
+ if (climbSpeed > 0) {
+ speedParts.add(String.format("climb %d ft.", climbSpeed));
+ }
+ if (flySpeed > 0) {
+ speedParts.add(String.format("fly %d ft.%s", flySpeed, canHover ? " (hover)" : ""));
+ }
+ if (swimSpeed > 0) {
+ speedParts.add(String.format("swim %d ft.", swimSpeed));
+ }
+ return StringHelper.join(", ", speedParts);
+ }
+ }
+
+ public String getStrengthDescription() {
+ return String.format("%d (%+d)", strengthScore, getStrengthModifier());
+ }
+
+ public String getDexterityDescription() {
+ return String.format("%d (%+d)", dexterityScore, getDexterityModifier());
+ }
+
+ public String getConstitutionDescription() {
+ return String.format("%d (%+d)", constitutionScore, getConstitutionModifier());
+ }
+
+ public String getIntelligenceDescription() {
+ return String.format("%d (%+d)", intelligenceScore, getIntelligenceModifier());
+ }
+
+ public String getWisdomDescription() {
+ return String.format("%d (%+d)", wisdomScore, getWisdomModifier());
+ }
+
+ public String getCharismaDescription() {
+ return String.format("%d (%+d)", charismaScore, getCharismaModifier());
+ }
+
+ public String getSavingThrowsDescription() {
+ List parts = new ArrayList<>();
+ for (AbilityScore abilityScore : AbilityScore.values()) {
+ AdvantageType advantage = getSavingThrowAdvantageType(abilityScore);
+ ProficiencyType proficiency = getSavingThrowProficiencyType(abilityScore);
+ if (advantage != AdvantageType.NONE || proficiency != ProficiencyType.NONE) {
+ int bonus = getAbilityModifier(abilityScore) + getProficiencyBonus(proficiency);
+ String part = String.format("%s %+d%s", abilityScore.displayName, bonus, advantage != AdvantageType.NONE ? " " + advantage.label : "");
+ parts.add(part);
+ }
+ }
+ return StringHelper.join(", ", parts);
+ }
+
+ public int getProficiencyBonus() {
+ ChallengeRating challengeRating = this.challengeRating != null ? this.challengeRating : ChallengeRating.ONE;
+ if (challengeRating == ChallengeRating.CUSTOM) {
+ return customProficiencyBonus;
+ } else {
+ return challengeRating.proficiencyBonus;
+ }
+ }
+
+ public int getProficiencyBonus(@NonNull ProficiencyType proficiencyType) {
+ switch (proficiencyType) {
+ case PROFICIENT:
+ return getProficiencyBonus();
+ case EXPERTISE:
+ return getProficiencyBonus() * 2;
+ case NONE:
+ default:
+ return 0;
+ }
+
+ }
+
+ public String getSkillsDescription() {
+ Skill[] elements = new Skill[skills.size()];
+ elements = skills.toArray(elements);
+ Arrays.sort(elements);
+ StringBuilder sb = new StringBuilder();
+ boolean isFirst = true;
+ for (Skill skill : elements) {
+ if (!isFirst) {
+ sb.append(", ");
+ }
+ sb.append(skill.getText(this));
+ isFirst = false;
+ }
+ return sb.toString();
+ }
+
+ public String getDamageVulnerabilitiesDescription() {
+ ArrayList vulnerabilities = new ArrayList<>();
+ for (String damageType : damageVulnerabilities) {
+ if (!StringHelper.isNullOrEmpty(damageType)) {
+ vulnerabilities.add(damageType);
+ }
+ }
+ Collections.sort(vulnerabilities);
+ return StringHelper.oxfordJoin(", ", ", and ", " and ", vulnerabilities);
+ }
+
+ public String getDamageResistancesDescription() {
+ ArrayList resistances = new ArrayList<>();
+ for (String damageType : damageResistances) {
+ if (!StringHelper.isNullOrEmpty(damageType)) {
+ resistances.add(damageType);
+ }
+ }
+ Collections.sort(resistances);
+ return StringHelper.oxfordJoin(", ", ", and ", " and ", resistances);
+ }
+
+ public String getDamageImmunitiesDescription() {
+ ArrayList immunities = new ArrayList<>();
+ for (String damageType : damageImmunities) {
+ if (!StringHelper.isNullOrEmpty(damageType)) {
+ immunities.add(damageType);
+ }
+ }
+ Collections.sort(immunities);
+ return StringHelper.oxfordJoin(", ", ", and ", " and ", immunities);
+ }
+
+ public String getConditionImmunitiesDescription() {
+ ArrayList immunities = new ArrayList<>(conditionImmunities);
+ Collections.sort(immunities);
+ return StringHelper.oxfordJoin(", ", ", and ", " and ", immunities);
+ }
+
+ public String getSensesDescription() {
+ ArrayList parts = new ArrayList<>(senses);
+ parts.add(String.format("passive Perception %d", 10 + getWisdomModifier()));
+ return StringHelper.join(", ", parts);
+ }
+
+ public String getLanguagesDescription() {
+ ArrayList spokenLanguages = new ArrayList<>();
+ ArrayList understoodLanguages = new ArrayList<>();
+ for (Language language : languages) {
+ if (language != null) {
+ if (language.getSpeaks()) {
+ spokenLanguages.add(language.getName());
+ } else {
+ understoodLanguages.add(language.getName());
+ }
+ }
+ }
+ Collections.sort(spokenLanguages);
+ Collections.sort(understoodLanguages);
+
+ String spokenLanguagesString = StringHelper.oxfordJoin(", ", ", and ", " and ", spokenLanguages);
+ String understoodLanguagesString = StringHelper.oxfordJoin(", ", ", and ", " and ", understoodLanguages);
+
+ boolean hasUnderstandsBut = understandsButDescription.length() > 0;
+ boolean hasTelepathy = telepathyRange > 0;
+ String telepathyString = String.format(", telepathy %d ft.", telepathyRange);
+
+ if (spokenLanguages.size() > 0) {
+ if (understoodLanguages.size() > 0) {
+ return String.format(
+ "%s, understands %s%s%s",
+ spokenLanguagesString,
+ understoodLanguagesString,
+ hasUnderstandsBut ? " but " + understandsButDescription : "",
+ hasTelepathy ? telepathyString : "");
+ } else {
+ return String.format(
+ "%s%s%s",
+ spokenLanguagesString,
+ hasUnderstandsBut ? " but " + understandsButDescription : "",
+ hasTelepathy ? telepathyString : "");
+ }
+ } else {
+ if (understoodLanguages.size() > 0) {
+ return String.format(
+ "understands %s%s%s",
+ understoodLanguagesString,
+ hasUnderstandsBut ? " but " + understandsButDescription : "",
+ hasTelepathy ? telepathyString : "");
+ } else {
+ return String.format(
+ "%S%s",
+ hasUnderstandsBut ? "none but " + understandsButDescription : "",
+ hasTelepathy ? telepathyString : "");
+ }
+ }
+ }
+
+ public String getChallengeRatingDescription() {
+ ChallengeRating challengeRating = this.challengeRating != null ? this.challengeRating : ChallengeRating.ONE;
+ if (challengeRating == ChallengeRating.CUSTOM) {
+ return customChallengeRatingDescription;
+ } else {
+ return challengeRating.displayName;
+ }
+ }
+
+ public List getAbilityDescriptions() {
+ ArrayList abilityDescriptions = new ArrayList<>();
+ for (Trait ability : abilities) {
+ abilityDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", ability.name, ability.description)));
+ }
+ return abilityDescriptions;
+ }
+
+ public String getPlaceholderReplacedText(@NonNull String rawText) {
+ return rawText
+ .replaceAll("\\[STR SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.STRENGTH)))
+ .replaceAll("\\[STR ATK]", String.format("%+d", getAttackBonus(AbilityScore.STRENGTH)))
+ .replaceAll("\\[DEX SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.DEXTERITY)))
+ .replaceAll("\\[DEX ATK]", String.format("%+d", getAttackBonus(AbilityScore.DEXTERITY)))
+ .replaceAll("\\[CON SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.CONSTITUTION)))
+ .replaceAll("\\[CON ATK]", String.format("%+d", getAttackBonus(AbilityScore.CONSTITUTION)))
+ .replaceAll("\\[INT SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.INTELLIGENCE)))
+ .replaceAll("\\[INT ATK]", String.format("%+d", getAttackBonus(AbilityScore.INTELLIGENCE)))
+ .replaceAll("\\[WIS SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.WISDOM)))
+ .replaceAll("\\[WIS ATK]", String.format("%+d", getAttackBonus(AbilityScore.WISDOM)))
+ .replaceAll("\\[CHA SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.CHARISMA)))
+ .replaceAll("\\[CHA ATK]", String.format("%+d", getAttackBonus(AbilityScore.CHARISMA)));
+ }
+
+ public int getSpellSaveDC(AbilityScore abilityScore) {
+ return 8 + getProficiencyBonus() + getAbilityModifier(abilityScore);
+ }
+
+ public int getAttackBonus(AbilityScore abilityScore) {
+ return getProficiencyBonus() + getAbilityModifier(abilityScore);
+ }
+
+ public List getActionDescriptions() {
+ ArrayList actionDescriptions = new ArrayList<>();
+ for (Trait action : actions) {
+ actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
+ }
+ return actionDescriptions;
+ }
+
+ public List getReactionDescriptions() {
+ ArrayList actionDescriptions = new ArrayList<>();
+ for (Trait action : reactions) {
+ actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
+ }
+ return actionDescriptions;
+ }
+
+ public List getLegendaryActionDescriptions() {
+ ArrayList actionDescriptions = new ArrayList<>();
+ for (Trait action : legendaryActions) {
+ actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
+ }
+ return actionDescriptions;
+ }
+
+ public List getLairActionDescriptions() {
+ ArrayList actionDescriptions = new ArrayList<>();
+ for (Trait action : lairActions) {
+ actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
+ }
+ return actionDescriptions;
+ }
+
+ public List getRegionalActionDescriptions() {
+ ArrayList actionDescriptions = new ArrayList<>();
+ for (Trait action : regionalActions) {
+ actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
+ }
+ return actionDescriptions;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof Monster)) {
+ return false;
+ }
+ Monster other = (Monster) obj;
+ if (!challengeRating.equals(other.challengeRating)) {
+ return false;
+ }
+ if (!understandsButDescription.equals(other.understandsButDescription)) {
+ return false;
+ }
+ if (!languages.equals(other.languages)) {
+ return false;
+ }
+ if (!damageVulnerabilities.equals(other.damageVulnerabilities)) {
+ return false;
+ }
+ if (!damageResistances.equals(other.damageResistances)) {
+ return false;
+ }
+ if (!damageImmunities.equals(other.damageImmunities)) {
+ return false;
+ }
+ if (!conditionImmunities.equals(other.conditionImmunities)) {
+ return false;
+ }
+ if (!charismaSavingThrowProficiency.equals(other.charismaSavingThrowProficiency)) {
+ return false;
+ }
+ if (!wisdomSavingThrowProficiency.equals(other.wisdomSavingThrowProficiency)) {
+ return false;
+ }
+ if (!intelligenceSavingThrowProficiency.equals(other.intelligenceSavingThrowProficiency)) {
+ return false;
+ }
+ if (constitutionSavingThrowProficiency.equals(other.constitutionSavingThrowProficiency)) {
+ return false;
+ }
+
+ if (dexteritySavingThrowProficiency.equals(other.dexteritySavingThrowProficiency)) {
+ return false;
+ }
+
+ if (strengthSavingThrowProficiency.equals(other.strengthSavingThrowProficiency)) {
+ return false;
+ }
+
+ if (legendaryActions.equals(other.legendaryActions)) {
+ return false;
+ }
+
+ if (customChallengeRatingDescription.equals(other.customChallengeRatingDescription)) {
+ return false;
+ }
+
+ if (customSpeedDescription.equals(other.customSpeedDescription)) {
+ return false;
+ }
+
+ if (customHPDescription.equals(other.customHPDescription)) {
+ return false;
+ }
+
+ if (otherArmorDescription.equals(other.otherArmorDescription)) {
+ return false;
+ }
+
+ if (alignment.equals(other.alignment)) {
+ return false;
+ }
+
+ if (subtype.equals(other.subtype)) {
+ return false;
+ }
+
+ if (abilities.equals(other.abilities)) {
+ return false;
+ }
+
+ if (actions.equals(other.actions)) {
+ return false;
+ }
+
+ if (armorType.equals(other.armorType)) {
+ return false;
+ }
+
+ if (charismaSavingThrowAdvantage.equals(other.charismaSavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (constitutionSavingThrowAdvantage.equals(other.constitutionSavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (dexteritySavingThrowAdvantage.equals(other.dexteritySavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (hitDice == other.hitDice) {
+ return false;
+ }
+
+ if (id.equals(other.id)) {
+ return false;
+ }
+
+ if (intelligenceSavingThrowAdvantage.equals(other.intelligenceSavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (lairActions.equals(other.lairActions)) {
+ return false;
+ }
+
+ if (name.equals(other.name)) {
+ return false;
+ }
+
+ if (reactions.equals(other.reactions)) {
+ return false;
+ }
+
+ if (regionalActions.equals(other.regionalActions)) {
+ return false;
+ }
+
+ if (senses.equals(other.senses)) {
+ return false;
+ }
+
+ if (shieldBonus == other.shieldBonus) {
+ return false;
+ }
+
+ if (size.equals(other.size)) {
+ return false;
+ }
+
+ if (skills.equals(other.skills)) {
+ return false;
+ }
+
+ if (strengthSavingThrowAdvantage.equals(other.strengthSavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (strengthScore == other.strengthScore) {
+ return false;
+ }
+
+ if (type.equals(other.type)) {
+ return false;
+ }
+
+ if (wisdomSavingThrowAdvantage.equals(other.wisdomSavingThrowAdvantage)) {
+ return false;
+ }
+
+ if (wisdomScore == other.wisdomScore) {
+ return false;
+ }
+ if (customProficiencyBonus == other.customProficiencyBonus) {
+ return false;
+ }
+
+ if (telepathyRange == other.telepathyRange) {
+ return false;
+ }
+
+ if (intelligenceScore == other.intelligenceScore) {
+ return false;
+ }
+
+ if (constitutionScore == other.constitutionScore) {
+ return false;
+ }
+
+ if (dexterityScore == other.dexterityScore) {
+ return false;
+ }
+
+ if (hasCustomSpeed == other.hasCustomSpeed) {
+ return false;
+ }
+
+ if (hasCustomHP == other.hasCustomHP) {
+ return false;
+ }
+
+ if (swimSpeed == other.swimSpeed) {
+ return false;
+ }
+
+ if (flySpeed == other.flySpeed) {
+ return false;
+ }
+
+ if (climbSpeed == other.climbSpeed) {
+ return false;
+ }
+
+ if (burrowSpeed == other.burrowSpeed) {
+ return false;
+ }
+
+ if (walkSpeed == other.walkSpeed) {
+ return false;
+ }
+
+ if (naturalArmorBonus == other.naturalArmorBonus) {
+ return false;
+ }
+
+ if (canHover == other.canHover) {
+ return false;
+ }
+
+ if (charismaScore == other.charismaScore) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static class Helpers {
+ public static int getAbilityModifierForScore(int score) {
+ return (int) Math.floor((score - 10) / 2.0);
+ }
+
+ public static int getHitDieForSize(String size) {
+ if ("tiny".equals(size)) {
+ return 4;
+ } else if ("small".equals(size)) {
+ return 6;
+ } else if ("medium".equals(size)) {
+ return 8;
+ } else if ("large".equals(size)) {
+ return 10;
+ } else if ("huge".equals(size)) {
+ return 12;
+ } else if ("gargantuan".equals(size)) {
+ return 20;
+ } else {
+ return 8;
+ }
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java
new file mode 100644
index 0000000..7d6c110
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java
@@ -0,0 +1,14 @@
+package com.majinnaibu.monstercards.models;
+
+import androidx.room.Entity;
+import androidx.room.Fts4;
+
+@Entity(tableName = "monsters_fts")
+@Fts4(contentEntity = Monster.class)
+public class MonsterFTS {
+ public String name;
+ public String size;
+ public String type;
+ public String subtype;
+ public String alignment;
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java
new file mode 100644
index 0000000..9730cc5
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java
@@ -0,0 +1,95 @@
+package com.majinnaibu.monstercards.models;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Nullable;
+
+import com.majinnaibu.monstercards.data.enums.AbilityScore;
+import com.majinnaibu.monstercards.data.enums.AdvantageType;
+import com.majinnaibu.monstercards.data.enums.ProficiencyType;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+@SuppressLint("DefaultLocale")
+public class Skill implements Comparator, Comparable {
+
+ public String name;
+ public AbilityScore abilityScore;
+ public AdvantageType advantageType;
+ public ProficiencyType proficiencyType;
+
+ public Skill(String name, AbilityScore abilityScore) {
+ this(name, abilityScore, AdvantageType.NONE, ProficiencyType.PROFICIENT);
+ }
+
+ public Skill(String name, AbilityScore abilityScore, AdvantageType advantageType) {
+ this(name, abilityScore, advantageType, ProficiencyType.PROFICIENT);
+ }
+
+ public Skill(String name, AbilityScore abilityScore, AdvantageType advantageType, ProficiencyType proficiencyType) {
+ this.name = name;
+ this.abilityScore = abilityScore;
+ this.advantageType = advantageType;
+ this.proficiencyType = proficiencyType;
+ }
+
+ public int getSkillBonus(Monster monster) {
+ int modifier = monster.getAbilityModifier(abilityScore);
+ switch (proficiencyType) {
+ case PROFICIENT:
+ return modifier + monster.getProficiencyBonus();
+ case EXPERTISE:
+ return modifier + monster.getProficiencyBonus() * 2;
+ case NONE:
+ default:
+ return modifier;
+ }
+ }
+
+ public String getText(Monster monster) {
+ int bonus = getSkillBonus(monster);
+
+ return String.format(
+ "%s%s %+d%s",
+ name.charAt(0),
+ name.substring(1),
+ bonus,
+ advantageType == AdvantageType.ADVANTAGE ? " A" : advantageType == AdvantageType.DISADVANTAGE ? " D" : ""
+ );
+ }
+
+ @Override
+ public int compareTo(Skill o) {
+ return this.name.compareToIgnoreCase(o.name);
+ }
+
+ @Override
+ public int compare(Skill o1, Skill o2) {
+ return o1.name.compareToIgnoreCase(o2.name);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof Skill)) {
+ return false;
+ }
+ Skill otherSkill = (Skill) obj;
+ if (!Objects.equals(this.name, otherSkill.name)) {
+ return false;
+ }
+ if (this.abilityScore != otherSkill.abilityScore) {
+ return false;
+ }
+ if (this.advantageType != otherSkill.advantageType) {
+ return false;
+ }
+ if (this.proficiencyType != otherSkill.proficiencyType) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java
new file mode 100644
index 0000000..f6a5703
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java
@@ -0,0 +1,49 @@
+package com.majinnaibu.monstercards.models;
+
+import androidx.annotation.Nullable;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+public class Trait implements Comparator, Comparable {
+
+ public String name;
+ public String description;
+
+ public Trait(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public int compareTo(Trait o) {
+ return compare(this, o);
+ }
+
+ @Override
+ public int compare(Trait o1, Trait o2) {
+ int result = o1.name.compareToIgnoreCase(o2.name);
+ if (result != 0) {
+ return result;
+ }
+ return o1.description.compareToIgnoreCase(o2.description);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof Trait)) {
+ return false;
+ }
+ Trait otherTrait = (Trait) obj;
+ if (!Objects.equals(this.name, otherTrait.name)) {
+ return false;
+ }
+ if (!Objects.equals(this.description, otherTrait.description)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java
new file mode 100644
index 0000000..a0fd733
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java
@@ -0,0 +1,27 @@
+package com.majinnaibu.monstercards.ui.collections;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+
+public class CollectionsFragment extends MCFragment {
+
+ private CollectionsViewModel collectionsViewModel;
+
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ collectionsViewModel = new ViewModelProvider(this).get(CollectionsViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_collections, container, false);
+ final TextView textView = root.findViewById(R.id.text_collections);
+ collectionsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
+ return root;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java
new file mode 100644
index 0000000..1ea5b1a
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java
@@ -0,0 +1,19 @@
+package com.majinnaibu.monstercards.ui.collections;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+public class CollectionsViewModel extends ViewModel {
+
+ private final MutableLiveData mText;
+
+ public CollectionsViewModel() {
+ mText = new MutableLiveData<>();
+ mText.setValue("This is collections fragment");
+ }
+
+ public LiveData getText() {
+ return mText;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java
new file mode 100644
index 0000000..6d15709
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java
@@ -0,0 +1,133 @@
+package com.majinnaibu.monstercards.ui.components;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.AbilityScore;
+import com.majinnaibu.monstercards.helpers.ArrayHelper;
+
+import java.util.Objects;
+
+public class AbilityScorePicker extends LinearLayout {
+ private final ViewHolder mHolder;
+ private OnValueChangedListener mOnValueChangedListener;
+ private AbilityScore mSelectedValue;
+ private String mLabel;
+
+ public AbilityScorePicker(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ mSelectedValue = AbilityScore.STRENGTH;
+ mOnValueChangedListener = null;
+ // TODO: use this as default but allow setting via attribute
+ mLabel = "Ability Score";
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AbilityScorePicker, 0, 0);
+ String label = a.getString(R.styleable.AbilityScorePicker_label);
+ if (label != null) {
+ mLabel = label;
+ }
+ a.recycle();
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.component_ability_score_picker, this, true);
+
+ mHolder = new ViewHolder(root);
+
+ mHolder.label.setText(mLabel);
+
+ mHolder.spinner.setAdapter(new ArrayAdapter(getContext(), R.layout.dropdown_list_item, AbilityScore.values()) {
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ AbilityScore item = getItem(position);
+ TextView view = (TextView) super.getView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ AbilityScore item = getItem(position);
+ TextView view = (TextView) super.getDropDownView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+ });
+ mHolder.spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ setValue((AbilityScore) parent.getItemAtPosition(position));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ setValue(mSelectedValue = AbilityScore.STRENGTH);
+ }
+ });
+ mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue));
+
+ setValue(AbilityScore.STRENGTH);
+ }
+
+ public AbilityScorePicker(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public AbilityScore getValue() {
+ return mSelectedValue;
+ }
+
+ public void setValue(AbilityScore value) {
+ if (value != mSelectedValue) {
+ mSelectedValue = value;
+ mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), value));
+ if (mOnValueChangedListener != null) {
+ mOnValueChangedListener.onValueChanged(value);
+ }
+ }
+ }
+
+ public void setOnValueChangedListener(OnValueChangedListener listener) {
+ mOnValueChangedListener = listener;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ if (!Objects.equals(mLabel, label)) {
+ mLabel = label;
+ mHolder.label.setText(label);
+ }
+ }
+
+ public interface OnValueChangedListener {
+ void onValueChanged(AbilityScore value);
+ }
+
+ private static class ViewHolder {
+
+ private final Spinner spinner;
+ private final TextView label;
+
+ ViewHolder(@NonNull View root) {
+ spinner = root.findViewById(R.id.spinner);
+ label = root.findViewById(R.id.label);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java
new file mode 100644
index 0000000..7ac572f
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java
@@ -0,0 +1,98 @@
+package com.majinnaibu.monstercards.ui.components;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RadioGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.google.android.material.radiobutton.MaterialRadioButton;
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.AdvantageType;
+
+@SuppressWarnings("unused")
+public class AdvantagePicker extends ConstraintLayout {
+ private final ViewHolder mHolder;
+ private OnValueChangedListener mOnValueChangedListener;
+ private AdvantageType mSelectedValue;
+
+ public AdvantagePicker(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ mSelectedValue = AdvantageType.NONE;
+ mOnValueChangedListener = null;
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.component_advantage_picker, this, true);
+
+ mHolder = new ViewHolder(root);
+
+ setValue(AdvantageType.NONE);
+ mHolder.group.setOnCheckedChangeListener((group, checkedId) -> {
+ if (R.id.hasAdvantage == checkedId) {
+ setValue(AdvantageType.ADVANTAGE);
+ } else if (R.id.hasDisadvantage == checkedId) {
+ setValue(AdvantageType.DISADVANTAGE);
+ } else {
+ setValue(AdvantageType.NONE);
+ }
+ });
+ }
+
+ public AdvantagePicker(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public AdvantageType getValue() {
+ return mSelectedValue;
+ }
+
+ public void setValue(AdvantageType value) {
+ if (mSelectedValue != value) {
+ mSelectedValue = value;
+ if (mOnValueChangedListener != null) {
+ mOnValueChangedListener.onValueChanged(mSelectedValue);
+ }
+ }
+ final int checkedId = mHolder.group.getCheckedRadioButtonId();
+ if (mSelectedValue == AdvantageType.ADVANTAGE) {
+ if (checkedId != R.id.hasAdvantage) {
+ mHolder.advantage.setChecked(true);
+ }
+ } else if (mSelectedValue == AdvantageType.DISADVANTAGE) {
+ if (checkedId != R.id.hasDisadvantage) {
+ mHolder.disadvantage.setChecked(true);
+ }
+ } else {
+ if (checkedId != R.id.none) {
+ mHolder.none.setChecked(true);
+ }
+ }
+ }
+
+ public void setOnValueChangedListener(OnValueChangedListener listener) {
+ mOnValueChangedListener = listener;
+ }
+
+ public interface OnValueChangedListener {
+ void onValueChanged(AdvantageType value);
+ }
+
+ private static class ViewHolder {
+ final RadioGroup group;
+ final MaterialRadioButton none;
+ final MaterialRadioButton advantage;
+ final MaterialRadioButton disadvantage;
+
+ ViewHolder(@NonNull View root) {
+ group = root.findViewById(R.id.group);
+ none = root.findViewById(R.id.hasNoAdvantage);
+ advantage = root.findViewById(R.id.hasAdvantage);
+ disadvantage = root.findViewById(R.id.hasDisadvantage);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java
new file mode 100644
index 0000000..8b33849
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java
@@ -0,0 +1,98 @@
+package com.majinnaibu.monstercards.ui.components;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RadioGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.google.android.material.radiobutton.MaterialRadioButton;
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.ProficiencyType;
+
+@SuppressWarnings("unused")
+public class ProficiencyPicker extends ConstraintLayout {
+ private final ViewHolder mHolder;
+ private OnValueChangedListener mOnValueChangedListener;
+ private ProficiencyType mSelectedValue;
+
+ public ProficiencyPicker(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ mSelectedValue = ProficiencyType.NONE;
+ mOnValueChangedListener = null;
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.component_proficiency_picker, this, true);
+
+ mHolder = new ViewHolder(root);
+
+ setValue(ProficiencyType.NONE);
+ mHolder.group.setOnCheckedChangeListener((group, checkedId) -> {
+ if (R.id.proficient == checkedId) {
+ setValue(ProficiencyType.PROFICIENT);
+ } else if (R.id.expertise == checkedId) {
+ setValue(ProficiencyType.EXPERTISE);
+ } else {
+ setValue(ProficiencyType.NONE);
+ }
+ });
+ }
+
+ public ProficiencyPicker(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public ProficiencyType getValue() {
+ return mSelectedValue;
+ }
+
+ public void setValue(ProficiencyType value) {
+ if (mSelectedValue != value) {
+ mSelectedValue = value;
+ if (mOnValueChangedListener != null) {
+ mOnValueChangedListener.onValueChanged(mSelectedValue);
+ }
+ }
+ final int checkedId = mHolder.group.getCheckedRadioButtonId();
+ if (mSelectedValue == ProficiencyType.PROFICIENT) {
+ if (checkedId != R.id.proficient) {
+ mHolder.proficient.setChecked(true);
+ }
+ } else if (mSelectedValue == ProficiencyType.EXPERTISE) {
+ if (checkedId != R.id.expertise) {
+ mHolder.expertise.setChecked(true);
+ }
+ } else {
+ if (checkedId != R.id.none) {
+ mHolder.none.setChecked(true);
+ }
+ }
+ }
+
+ public void setOnValueChangedListener(OnValueChangedListener listener) {
+ mOnValueChangedListener = listener;
+ }
+
+ public interface OnValueChangedListener {
+ void onValueChanged(ProficiencyType value);
+ }
+
+ private static class ViewHolder {
+ final RadioGroup group;
+ final MaterialRadioButton none;
+ final MaterialRadioButton proficient;
+ final MaterialRadioButton expertise;
+
+ ViewHolder(@NonNull View root) {
+ group = root.findViewById(R.id.group);
+ none = root.findViewById(R.id.none);
+ proficient = root.findViewById(R.id.proficient);
+ expertise = root.findViewById(R.id.expertise);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java
new file mode 100644
index 0000000..911a5b9
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java
@@ -0,0 +1,149 @@
+package com.majinnaibu.monstercards.ui.components;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.majinnaibu.monstercards.R;
+
+import java.util.Objects;
+
+
+@SuppressWarnings("unused")
+public class Stepper extends ConstraintLayout {
+ private final ViewHolder mHolder;
+ private int mCurrentValue;
+ private int mStep;
+ private int mMinValue;
+ private int mMaxValue;
+ private String mLabel;
+ private OnValueChangeListener mOnValueChangeListener;
+ private OnFormatValueCallback mOnFormatValueCallback;
+
+ public Stepper(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ mCurrentValue = 0;
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Stepper, 0, 0);
+ mStep = a.getInt(R.styleable.Stepper_stepAmount, 1);
+ mMinValue = a.getInt(R.styleable.Stepper_minValue, Integer.MIN_VALUE);
+ mMaxValue = a.getInt(R.styleable.Stepper_maxValue, Integer.MAX_VALUE);
+ mLabel = a.getString(R.styleable.Stepper_label);
+ a.recycle();
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.component_stepper, this, true);
+
+ mHolder = new ViewHolder(root);
+
+ setValue(mCurrentValue);
+ updateDisplayedValue();
+ mHolder.increment.setOnClickListener(v -> setValue(mCurrentValue + mStep));
+ mHolder.decrement.setOnClickListener(v -> setValue(mCurrentValue - mStep));
+
+ mHolder.label.setText(mLabel);
+ }
+
+ public Stepper(Context context) {
+ this(context, null);
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String newLabel) {
+ if (!Objects.equals(mLabel, newLabel)) {
+ mLabel = newLabel;
+ mHolder.label.setText(mLabel);
+ }
+ }
+
+ public int getValue() {
+ return mCurrentValue;
+ }
+
+ public void setValue(int value) {
+ int oldValue = this.mCurrentValue;
+ int newValue = Math.min(mMaxValue, Math.max(mMinValue, value));
+ if (newValue != oldValue) {
+ this.mCurrentValue = newValue;
+ if (mOnValueChangeListener != null) {
+ mOnValueChangeListener.onChange(newValue, oldValue);
+ }
+ updateDisplayedValue();
+ }
+ }
+
+ private void updateDisplayedValue() {
+ if (mOnFormatValueCallback != null) {
+ mHolder.text.setText(mOnFormatValueCallback.onFormatValue(this.mCurrentValue));
+ } else {
+ mHolder.text.setText(String.valueOf(this.mCurrentValue));
+ }
+ }
+
+ public void setOnValueChangeListener(OnValueChangeListener listener) {
+ mOnValueChangeListener = listener;
+ }
+
+ public void setOnFormatValueCallback(OnFormatValueCallback callback) {
+ mOnFormatValueCallback = callback;
+ updateDisplayedValue();
+ }
+
+ public int getStep() {
+ return mStep;
+ }
+
+ public void setStep(int step) {
+ this.mStep = step;
+ }
+
+ public int getMinValue() {
+ return mMinValue;
+ }
+
+ public void setMinValue(int minValue) {
+ this.mMinValue = minValue;
+ }
+
+ public int getMaxValue() {
+ return mMaxValue;
+ }
+
+ public void setMaxValue(int maxValue) {
+ this.mMaxValue = maxValue;
+ }
+
+ public interface OnValueChangeListener {
+ void onChange(int value, int previousValue);
+ }
+
+ public interface OnFormatValueCallback {
+ String onFormatValue(int value);
+ }
+
+ private static class ViewHolder {
+ final TextView text;
+ final TextView label;
+ final Button increment;
+ final Button decrement;
+
+ ViewHolder(@NonNull View root) {
+ text = root.findViewById(R.id.text);
+ label = root.findViewById(R.id.label);
+ increment = root.findViewById(R.id.increment);
+ decrement = root.findViewById(R.id.decrement);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java
new file mode 100644
index 0000000..92e61ce
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java
@@ -0,0 +1,84 @@
+package com.majinnaibu.monstercards.ui.dashboard;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.models.Monster;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.util.List;
+import java.util.Locale;
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+public class DashboardFragment extends MCFragment {
+ private DashboardViewModel mViewModel;
+ private ViewHolder mHolder;
+ private DashboardRecyclerViewAdapter mAdapter;
+
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
+ mHolder = new ViewHolder(root);
+
+ setupRecyclerView(mHolder.list);
+
+ getMonsterRepository()
+ .getMonsters()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(monsters -> mViewModel.setMonsters(monsters));
+
+ return root;
+ }
+
+ private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
+ int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396);
+ Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount));
+ Context context = requireContext();
+ GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
+ recyclerView.setLayoutManager(layoutManager);
+
+ LiveData> monsterData = mViewModel.getMonsters();
+ mAdapter = new DashboardRecyclerViewAdapter(monster -> {
+ if (monster != null) {
+ navigateToMonsterDetail(monster);
+ } else {
+ Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
+ }
+ });
+ if (monsterData != null) {
+ monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
+ }
+ recyclerView.setAdapter(mAdapter);
+ }
+
+ private void navigateToMonsterDetail(Monster monster) {
+ NavDirections action = DashboardFragmentDirections.actionNavigationDashboardToNavigationMonster(monster.id.toString());
+ Navigation.findNavController(requireView()).navigate(action);
+ }
+
+ private static class ViewHolder {
+ final RecyclerView list;
+
+ ViewHolder(View root) {
+ list = root.findViewById(R.id.list);
+ }
+ }
+
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java
new file mode 100644
index 0000000..d6e6149
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java
@@ -0,0 +1,353 @@
+package com.majinnaibu.monstercards.ui.dashboard;
+
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.AbilityScore;
+import com.majinnaibu.monstercards.data.enums.AdvantageType;
+import com.majinnaibu.monstercards.data.enums.ChallengeRating;
+import com.majinnaibu.monstercards.data.enums.ProficiencyType;
+import com.majinnaibu.monstercards.databinding.CardMonsterBinding;
+import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
+import com.majinnaibu.monstercards.models.Monster;
+import com.majinnaibu.monstercards.models.Trait;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.util.Locale;
+
+public class DashboardRecyclerViewAdapter extends ListAdapter {
+ private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
+ return oldItem.id.equals(newItem.id);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
+ return oldItem.equals(newItem);
+ }
+ };
+ private final ItemCallback mOnClick;
+
+ protected DashboardRecyclerViewAdapter(ItemCallback onClick) {
+ super(DIFF_CALLBACK);
+ mOnClick = onClick;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(CardMonsterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ Logger.logUnimplementedMethod();
+ Monster monster = getItem(position);
+ holder.monster = monster;
+ holder.name.setText(monster.name);
+ holder.meta.setText(monster.getMeta());
+ holder.strengthAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.strengthSavingThrowAdvantage));
+ holder.strengthModifier.setText(Helpers.getModifierString(monster.getStrengthModifier()));
+ holder.strengthName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.STRENGTH));
+ holder.strengthProficiency.setText(Helpers.getProficiencyAbbreviation(monster.strengthSavingThrowProficiency));
+
+ holder.dexterityAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.dexteritySavingThrowAdvantage));
+ holder.dexterityModifier.setText(Helpers.getModifierString(monster.getDexterityModifier()));
+ holder.dexterityName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.DEXTERITY));
+ holder.dexterityProficiency.setText(Helpers.getProficiencyAbbreviation(monster.dexteritySavingThrowProficiency));
+
+ holder.constitutionAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.constitutionSavingThrowAdvantage));
+ holder.constitutionModifier.setText(Helpers.getModifierString(monster.getConstitutionModifier()));
+ holder.constitutionName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CONSTITUTION));
+ holder.constitutionProficiency.setText(Helpers.getProficiencyAbbreviation(monster.constitutionSavingThrowProficiency));
+
+ holder.intelligenceAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.intelligenceSavingThrowAdvantage));
+ holder.intelligenceModifier.setText(Helpers.getModifierString(monster.getIntelligenceModifier()));
+ holder.intelligenceName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.INTELLIGENCE));
+ holder.intelligenceProficiency.setText(Helpers.getProficiencyAbbreviation(monster.intelligenceSavingThrowProficiency));
+
+ holder.wisdomAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.wisdomSavingThrowAdvantage));
+ holder.wisdomModifier.setText(Helpers.getModifierString(monster.getWisdomModifier()));
+ holder.wisdomName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.WISDOM));
+ holder.wisdomProficiency.setText(Helpers.getProficiencyAbbreviation(monster.wisdomSavingThrowProficiency));
+
+ holder.charismaAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.charismaSavingThrowAdvantage));
+ holder.charismaModifier.setText(Helpers.getModifierString(monster.getCharismaModifier()));
+ holder.charismaName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CHARISMA));
+ holder.charismaProficiency.setText(Helpers.getProficiencyAbbreviation(monster.charismaSavingThrowProficiency));
+
+ holder.armorClass.setText(String.valueOf(monster.getArmorClassValue()));
+ holder.hitPoints.setText(String.valueOf(monster.getHitPointsValue()));
+ holder.challengeRating.setText(holder.challengeRating.getResources().getString(R.string.label_challenge_rating_with_value, Helpers.getChallengeRatingAbbreviation(monster.challengeRating)));
+
+ int numActions = monster.actions.size();
+ if (numActions > 0) {
+ holder.action1Group.setVisibility(View.VISIBLE);
+ Trait action = monster.actions.get(0);
+ holder.action1Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
+ holder.action1Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
+ } else {
+ holder.action1Group.setVisibility(View.GONE);
+ }
+
+ if (numActions > 1) {
+ holder.action2Group.setVisibility(View.VISIBLE);
+ Trait action = monster.actions.get(1);
+ holder.action2Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
+ holder.action2Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
+ } else {
+ holder.action2Group.setVisibility(View.GONE);
+ }
+
+ if (numActions > 2) {
+ holder.action3Group.setVisibility(View.VISIBLE);
+ Trait action = monster.actions.get(2);
+ holder.action3Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
+ holder.action3Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
+ } else {
+ holder.action3Group.setVisibility(View.GONE);
+ }
+
+ holder.itemView.setOnClickListener(v -> {
+ if (mOnClick != null) {
+ mOnClick.onItemCallback(holder.monster);
+ }
+ });
+ }
+
+ public interface ItemCallback {
+ void onItemCallback(Monster monster);
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public final TextView name;
+ public final TextView meta;
+ public final View action1Group;
+ public final TextView action1Name;
+ public final TextView action1Description;
+ public final View action2Group;
+ public final TextView action2Name;
+ public final TextView action2Description;
+ public final View action3Group;
+ public final TextView action3Name;
+ public final TextView action3Description;
+ public final TextView strengthName;
+ public final TextView strengthModifier;
+ public final TextView strengthProficiency;
+ public final TextView strengthAdvantage;
+ public final TextView dexterityName;
+ public final TextView dexterityModifier;
+ public final TextView dexterityProficiency;
+ public final TextView dexterityAdvantage;
+ public final TextView constitutionName;
+ public final TextView constitutionModifier;
+ public final TextView constitutionProficiency;
+ public final TextView constitutionAdvantage;
+ public final TextView intelligenceName;
+ public final TextView intelligenceModifier;
+ public final TextView intelligenceProficiency;
+ public final TextView intelligenceAdvantage;
+ public final TextView wisdomName;
+ public final TextView wisdomModifier;
+ public final TextView wisdomProficiency;
+ public final TextView wisdomAdvantage;
+ public final TextView charismaName;
+ public final TextView charismaModifier;
+ public final TextView charismaProficiency;
+ public final TextView charismaAdvantage;
+ public final TextView armorClass;
+ public final TextView hitPoints;
+ public final TextView challengeRating;
+ public Monster monster;
+
+ public ViewHolder(@NonNull CardMonsterBinding binding) {
+ super(binding.getRoot());
+ name = binding.name;
+ meta = binding.meta;
+ action1Group = binding.action1.getRoot();
+ action1Name = binding.action1.name;
+ action1Description = binding.action1.description;
+ action2Group = binding.action2.getRoot();
+ action2Name = binding.action2.name;
+ action2Description = binding.action2.description;
+ action3Group = binding.action3.getRoot();
+ action3Name = binding.action3.name;
+ action3Description = binding.action3.description;
+ strengthName = binding.strength.name;
+ strengthModifier = binding.strength.modifier;
+ strengthProficiency = binding.strength.proficiency;
+ strengthAdvantage = binding.strength.advantage;
+ dexterityName = binding.dexterity.name;
+ dexterityModifier = binding.dexterity.modifier;
+ dexterityProficiency = binding.dexterity.proficiency;
+ dexterityAdvantage = binding.dexterity.advantage;
+ constitutionName = binding.constitution.name;
+ constitutionModifier = binding.constitution.modifier;
+ constitutionProficiency = binding.constitution.proficiency;
+ constitutionAdvantage = binding.constitution.advantage;
+ intelligenceName = binding.intelligence.name;
+ intelligenceModifier = binding.intelligence.modifier;
+ intelligenceProficiency = binding.intelligence.proficiency;
+ intelligenceAdvantage = binding.intelligence.advantage;
+ wisdomName = binding.wisdom.name;
+ wisdomModifier = binding.wisdom.modifier;
+ wisdomProficiency = binding.wisdom.proficiency;
+ wisdomAdvantage = binding.wisdom.advantage;
+ charismaName = binding.charisma.name;
+ charismaModifier = binding.charisma.modifier;
+ charismaProficiency = binding.charisma.proficiency;
+ charismaAdvantage = binding.charisma.advantage;
+ armorClass = binding.armorClass.value;
+ hitPoints = binding.hitPoints.value;
+ challengeRating = binding.challengeRating;
+ }
+ }
+
+ public static class Helpers {
+ @NonNull
+ public static String getModifierString(int value) {
+ return String.format(Locale.getDefault(), "%+d", value);
+ }
+
+ @NonNull
+ public static String getAbilityScoreAbbreviation(@NonNull AbilityScore abilityScore) {
+ switch (abilityScore) {
+ case STRENGTH:
+ return "S";
+ case DEXTERITY:
+ return "D";
+ case CONSTITUTION:
+ return "C";
+ case INTELLIGENCE:
+ return "I";
+ case WISDOM:
+ return "W";
+ case CHARISMA:
+ return "Ch";
+ default:
+ Logger.logUnimplementedFeature(String.format("Get an abbreviation for AbilityScore value %s", abilityScore));
+ return "";
+ }
+ }
+
+ @NonNull
+ public static String getChallengeRatingAbbreviation(@NonNull ChallengeRating challengeRating) {
+ Logger.logUnimplementedMethod();
+ switch (challengeRating) {
+ case CUSTOM:
+ return "*";
+ case ZERO:
+ return "0";
+ case ONE_EIGHTH:
+ return "1/8";
+ case ONE_QUARTER:
+ return "1/4";
+ case ONE_HALF:
+ return "1/2";
+ case ONE:
+ return "1";
+ case TWO:
+ return "2";
+ case THREE:
+ return "3";
+ case FOUR:
+ return "4";
+ case FIVE:
+ return "5";
+ case SIX:
+ return "6";
+ case SEVEN:
+ return "7";
+ case EIGHT:
+ return "8";
+ case NINE:
+ return "9";
+ case TEN:
+ return "10";
+ case ELEVEN:
+ return "11";
+ case TWELVE:
+ return "12";
+ case THIRTEEN:
+ return "13";
+ case FOURTEEN:
+ return "14";
+ case FIFTEEN:
+ return "15";
+ case SIXTEEN:
+ return "16";
+ case SEVENTEEN:
+ return "17";
+ case EIGHTEEN:
+ return "18";
+ case NINETEEN:
+ return "19";
+ case TWENTY:
+ return "20";
+ case TWENTY_ONE:
+ return "21";
+ case TWENTY_TWO:
+ return "22";
+ case TWENTY_THREE:
+ return "23";
+ case TWENTY_FOUR:
+ return "24";
+ case TWENTY_FIVE:
+ return "25";
+ case TWENTY_SIX:
+ return "26";
+ case TWENTY_SEVEN:
+ return "27";
+ case TWENTY_EIGHT:
+ return "28";
+ case TWENTY_NINE:
+ return "29";
+ case THIRTY:
+ return "30";
+ default:
+ Logger.logUnimplementedFeature(String.format("Get an abbreviation for ChallengeRating value %s", challengeRating));
+ return "";
+ }
+ }
+
+ @NonNull
+ public static String getProficiencyAbbreviation(@NonNull ProficiencyType proficiency) {
+ switch (proficiency) {
+ case NONE:
+ return "";
+ case EXPERTISE:
+ return "E";
+ case PROFICIENT:
+ return "P";
+ default:
+ Logger.logUnimplementedFeature(String.format("Get an abbreviation for ProficiencyType value %s", proficiency));
+ return "";
+ }
+ }
+
+ @NonNull
+ public static String getAdvantageAbbreviation(@NonNull AdvantageType advantage) {
+ switch (advantage) {
+ case NONE:
+ return "";
+ case ADVANTAGE:
+ return "A";
+ case DISADVANTAGE:
+ return "D";
+ default:
+ Logger.logUnimplementedFeature(String.format("Get an abbreviation for AdvantageType value %s", advantage));
+ return "";
+ }
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java
new file mode 100644
index 0000000..98b6711
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java
@@ -0,0 +1,26 @@
+package com.majinnaibu.monstercards.ui.dashboard;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.majinnaibu.monstercards.models.Monster;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DashboardViewModel extends ViewModel {
+ private final MutableLiveData> mMonsters;
+
+ public DashboardViewModel() {
+ mMonsters = new MutableLiveData<>(new ArrayList<>());
+ }
+
+ public LiveData> getMonsters() {
+ return mMonsters;
+ }
+
+ public void setMonsters(List monsters) {
+ mMonsters.setValue(monsters);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java
new file mode 100644
index 0000000..f22c94d
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java
@@ -0,0 +1,82 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.ui.components.Stepper;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+
+import java.util.Locale;
+
+public class EditAbilityScoresFragment extends MCFragment {
+ private final String ABILITY_SCORE_FORMAT = "%d (%+d)";
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ private int getModifier(int value) {
+ return value / 2 - 5;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_ability_scores, container, false);
+ mHolder = new ViewHolder(root);
+
+ mViewModel.getStrength().observe(getViewLifecycleOwner(), value -> mHolder.strength.setValue(value));
+ mHolder.strength.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setStrength(newValue));
+ mHolder.strength.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ mViewModel.getDexterity().observe(getViewLifecycleOwner(), value -> mHolder.dexterity.setValue(value));
+ mHolder.dexterity.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setDexterity(newValue));
+ mHolder.dexterity.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ mViewModel.getConstitution().observe(getViewLifecycleOwner(), value -> mHolder.constitution.setValue(value));
+ mHolder.constitution.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setConstitution(newValue));
+ mHolder.constitution.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ mViewModel.getIntelligence().observe(getViewLifecycleOwner(), value -> mHolder.intelligence.setValue(value));
+ mHolder.intelligence.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setIntelligence(newValue));
+ mHolder.intelligence.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ mViewModel.getWisdom().observe(getViewLifecycleOwner(), value -> mHolder.wisdom.setValue(value));
+ mHolder.wisdom.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setWisdom(newValue));
+ mHolder.wisdom.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ mViewModel.getCharisma().observe(getViewLifecycleOwner(), value -> mHolder.charisma.setValue(value));
+ mHolder.charisma.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setCharisma(newValue));
+ mHolder.charisma.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
+
+ return root;
+ }
+
+ private static class ViewHolder {
+ final Stepper strength;
+ final Stepper dexterity;
+ final Stepper constitution;
+ final Stepper intelligence;
+ final Stepper wisdom;
+ final Stepper charisma;
+
+ ViewHolder(@NonNull View root) {
+ strength = root.findViewById(R.id.strength);
+ dexterity = root.findViewById(R.id.dexterity);
+ constitution = root.findViewById(R.id.constitution);
+ intelligence = root.findViewById(R.id.intelligence);
+ wisdom = root.findViewById(R.id.wisdom);
+ charisma = root.findViewById(R.id.charisma);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java
new file mode 100644
index 0000000..a50534e
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java
@@ -0,0 +1,104 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.ArmorType;
+import com.majinnaibu.monstercards.helpers.ArrayHelper;
+import com.majinnaibu.monstercards.ui.components.Stepper;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.TextChangedListener;
+
+public class EditArmorFragment extends MCFragment {
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_armor, container, false);
+ mHolder = new ViewHolder(root);
+
+ mHolder.armorType.setAdapter(new ArrayAdapter(requireContext(), R.layout.dropdown_list_item, ArmorType.values()) {
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ArmorType item = getItem(position);
+ TextView view = (TextView) super.getView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ArmorType item = getItem(position);
+ TextView view = (TextView) super.getDropDownView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+ });
+ mHolder.armorType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ ArmorType selectedItem = (ArmorType) parent.getItemAtPosition(position);
+ mViewModel.setArmorType(selectedItem);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ mViewModel.setArmorType(ArmorType.NONE);
+ }
+ });
+ mHolder.armorType.setSelection(ArrayHelper.indexOf(ArmorType.values(), mViewModel.getArmorType().getValue()));
+
+ mHolder.naturalArmorBonus.setValue(mViewModel.getNaturalArmorBonusUnboxed());
+ mHolder.naturalArmorBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setNaturalArmorBonus(newValue));
+
+ mHolder.hasShield.setChecked(mViewModel.getHasShieldValueAsBoolean());
+ mHolder.hasShield.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setHasShield(isChecked));
+
+ mHolder.shieldBonus.setValue(mViewModel.getShieldBonusUnboxed());
+ mHolder.shieldBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setShieldBonus(newValue));
+
+ mHolder.customArmor.setText(mViewModel.getCustomArmor().getValue());
+ mHolder.customArmor.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomArmor(s.toString()))));
+
+ return root;
+ }
+
+ private static class ViewHolder {
+ private final Spinner armorType;
+ private final Stepper naturalArmorBonus;
+ private final SwitchCompat hasShield;
+ private final Stepper shieldBonus;
+ private final EditText customArmor;
+
+ ViewHolder(@NonNull View root) {
+ armorType = root.findViewById(R.id.armorType);
+ naturalArmorBonus = root.findViewById(R.id.naturalArmorBonus);
+ hasShield = root.findViewById(R.id.hasShield);
+ shieldBonus = root.findViewById(R.id.shieldBonus);
+ customArmor = root.findViewById(R.id.customArmor);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java
new file mode 100644
index 0000000..3fba542
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java
@@ -0,0 +1,88 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+import com.google.android.material.switchmaterial.SwitchMaterial;
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.ui.components.Stepper;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.TextChangedListener;
+
+public class EditBasicInfoFragment extends MCFragment {
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHolder.name.requestFocus();
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_basic_info, container, false);
+ mHolder = new ViewHolder(root);
+
+ mHolder.name.setText(mViewModel.getName().getValue());
+ mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
+
+ mHolder.size.setText(mViewModel.getSize().getValue());
+ mHolder.size.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSize(s.toString())));
+
+ mHolder.type.setText(mViewModel.getType().getValue());
+ mHolder.type.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setType(s.toString())));
+
+ mHolder.subtype.setText(mViewModel.getSubtype().getValue());
+ mHolder.subtype.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSubtype(s.toString())));
+
+ mHolder.alignment.setText(mViewModel.getAlignment().getValue());
+ mHolder.alignment.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setAlignment(s.toString())));
+
+ mHolder.customHitPoints.setText(mViewModel.getCustomHitPoints().getValue());
+ mHolder.customHitPoints.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomHitPoints(s.toString()))));
+
+ mHolder.hitDice.setValue(mViewModel.getHitDiceUnboxed());
+ mHolder.hitDice.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setHitDice(newValue));
+
+ mHolder.hasCustomHitPoints.setChecked(mViewModel.getHasCustomHitPointsValueAsBoolean());
+ mHolder.hasCustomHitPoints.setOnCheckedChangeListener((button, isChecked) -> mViewModel.setHasCustomHitPoints(isChecked));
+
+ return root;
+ }
+
+ private static class ViewHolder {
+ private final EditText name;
+ private final EditText size;
+ private final EditText type;
+ private final EditText subtype;
+ private final EditText alignment;
+ private final EditText customHitPoints;
+ private final Stepper hitDice;
+ private final SwitchMaterial hasCustomHitPoints;
+
+ ViewHolder(@NonNull View root) {
+ name = root.findViewById(R.id.name);
+ size = root.findViewById(R.id.size);
+ type = root.findViewById(R.id.type);
+ subtype = root.findViewById(R.id.subtype);
+ alignment = root.findViewById(R.id.alignment);
+ customHitPoints = root.findViewById(R.id.customHitPoints);
+ hitDice = root.findViewById(R.id.hitDice);
+ hasCustomHitPoints = root.findViewById(R.id.hasCustomHitPoints);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java
new file mode 100644
index 0000000..33196cd
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java
@@ -0,0 +1,91 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.enums.ChallengeRating;
+import com.majinnaibu.monstercards.helpers.ArrayHelper;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.TextChangedListener;
+
+public class EditChallengeRatingFragment extends MCFragment {
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_challenge_rating, container, false);
+ mHolder = new ViewHolder(root);
+
+ mHolder.challengeRating.setAdapter(new ArrayAdapter(requireContext(), R.layout.dropdown_list_item, ChallengeRating.values()) {
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ChallengeRating item = getItem(position);
+ TextView view = (TextView) super.getView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ChallengeRating item = getItem(position);
+ TextView view = (TextView) super.getDropDownView(position, convertView, parent);
+ view.setText(item.displayName);
+ return view;
+ }
+ });
+ mHolder.challengeRating.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ ChallengeRating selectedItem = (ChallengeRating) parent.getItemAtPosition(position);
+ mViewModel.setChallengeRating(selectedItem);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ mViewModel.setChallengeRating(ChallengeRating.CUSTOM);
+ }
+ });
+ mHolder.challengeRating.setSelection(ArrayHelper.indexOf(ChallengeRating.values(), mViewModel.getChallengeRating().getValue()));
+
+ mHolder.customChallengeRatingDescription.setText(mViewModel.getCustomChallengeRatingDescription().getValue());
+ mHolder.customChallengeRatingDescription.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomChallengeRatingDescription(s.toString()))));
+
+ mHolder.customProficiencyBonus.setText(mViewModel.getCustomProficiencyBonusValueAsString());
+ mHolder.customProficiencyBonus.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomProficiencyBonus(s.toString()))));
+
+ return root;
+ }
+
+ private static class ViewHolder {
+ final Spinner challengeRating;
+ final EditText customChallengeRatingDescription;
+ final EditText customProficiencyBonus;
+
+ ViewHolder(@NonNull View root) {
+ challengeRating = root.findViewById(R.id.challengeRating);
+ customChallengeRatingDescription = root.findViewById(R.id.customChallengeRatingDescription);
+ customProficiencyBonus = root.findViewById(R.id.customProficiencyBonus);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java
new file mode 100644
index 0000000..bc39d8d
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java
@@ -0,0 +1,88 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.models.Language;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.Logger;
+import com.majinnaibu.monstercards.utils.TextChangedListener;
+
+public class EditLanguageFragment extends MCFragment {
+ private EditMonsterViewModel mEditMonsterViewModel;
+ private EditLanguageViewModel mViewModel;
+ private ViewHolder mHolder;
+ private Language mOldLanguage;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ mViewModel = new ViewModelProvider(this).get(EditLanguageViewModel.class);
+ Bundle arguments = getArguments();
+ if (arguments != null) {
+ EditLanguageFragmentArgs args = EditLanguageFragmentArgs.fromBundle(arguments);
+ mOldLanguage = new Language(args.getName(), args.getCanSpeak());
+ mViewModel.copyFromLanguage(mOldLanguage);
+ } else {
+ Logger.logWTF("EditLanguageFragment needs arguments");
+ mOldLanguage = null;
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_language, container, false);
+ mHolder = new ViewHolder(root);
+
+ mHolder.name.setText(mViewModel.getName().getValue());
+ mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
+
+ mHolder.canSpeak.setChecked(mViewModel.getCanSpeakValue());
+ mHolder.canSpeak.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanSpeak(isChecked));
+
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ if (mViewModel.hasChanges()) {
+ mEditMonsterViewModel.replaceLanguage(mOldLanguage, mViewModel.getLanguage().getValue());
+ }
+ Navigation.findNavController(requireView()).navigateUp();
+ }
+ });
+
+ return root;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHolder.name.requestFocus();
+ }
+
+ private static class ViewHolder {
+ EditText name;
+ SwitchCompat canSpeak;
+
+ ViewHolder(@NonNull View root) {
+ name = root.findViewById(R.id.name);
+ canSpeak = root.findViewById(R.id.canSpeak);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java
new file mode 100644
index 0000000..1b38692
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java
@@ -0,0 +1,68 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.majinnaibu.monstercards.models.Language;
+import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
+import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
+
+public class EditLanguageViewModel extends ChangeTrackedViewModel {
+ private final ChangeTrackedLiveData mName;
+ private final ChangeTrackedLiveData mCanSpeak;
+ private final ChangeTrackedLiveData mLanguage;
+
+ public EditLanguageViewModel() {
+ super();
+ mName = new ChangeTrackedLiveData<>("New Language", this::makeDirty);
+ mCanSpeak = new ChangeTrackedLiveData<>(true, this::makeDirty);
+ mLanguage = new ChangeTrackedLiveData<>(makeLanguage(), this::makeDirty);
+ }
+
+ public void copyFromLanguage(@NonNull Language language) {
+ mName.resetValue(language.getName());
+ mCanSpeak.resetValue(language.getSpeaks());
+ makeClean();
+ }
+
+ public LiveData getLanguage() {
+ return mLanguage;
+ }
+
+ public LiveData getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName.setValue(name);
+ mLanguage.setValue(makeLanguage());
+ }
+
+ public LiveData getCanSpeak() {
+ return mCanSpeak;
+ }
+
+ public void setCanSpeak(boolean canSpeak) {
+ mCanSpeak.setValue(canSpeak);
+ mLanguage.setValue(makeLanguage());
+ }
+
+ public boolean getCanSpeakValue(boolean defaultIfNull) {
+ Boolean boxedValue = mCanSpeak.getValue();
+ if (boxedValue == null) {
+ return defaultIfNull;
+ }
+ return boxedValue;
+ }
+
+ public boolean getCanSpeakValue() {
+ return getCanSpeakValue(false);
+ }
+
+ @NonNull
+ private Language makeLanguage() {
+ Boolean boxedValue = mCanSpeak.getValue();
+ boolean canSpeak = boxedValue != null && boxedValue;
+ return new Language(mName.getValue(), canSpeak);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java
new file mode 100644
index 0000000..de9d70c
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java
@@ -0,0 +1,101 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+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.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.models.Language;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
+import com.majinnaibu.monstercards.utils.Logger;
+import com.majinnaibu.monstercards.utils.TextChangedListener;
+
+public class EditLanguagesFragment extends MCFragment {
+ // TODO: Make the swipe to delete not happen for the header
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ private void navigateToEditLanguage(@NonNull Language language) {
+ NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks());
+ Navigation.findNavController(requireView()).navigate(action);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+ View root = inflater.inflate(R.layout.fragment_edit_languages_list, container, false);
+ mHolder = new ViewHolder(root);
+ setupRecyclerView(mHolder.list);
+ setupAddLanguageButton(mHolder.addLanguage);
+
+ return root;
+ }
+
+ private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
+ Context context = requireContext();
+ LinearLayoutManager layoutManager = new LinearLayoutManager(context);
+ recyclerView.setLayoutManager(layoutManager);
+
+ mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> {
+ EditLanguagesRecyclerViewAdapter adapter = new EditLanguagesRecyclerViewAdapter(
+ mViewModel.getLanguagesArray(),
+ language -> {
+ if (language != null) {
+ navigateToEditLanguage(language);
+ } else {
+ Logger.logError("Can't navigate to EditSkill with a null skill");
+ }
+ },
+ mViewModel.getTelepathyRangeUnboxed(),
+ (value, previousValue) -> mViewModel.setTelepathyRange(value),
+ mViewModel.getUnderstandsButDescription().getValue(),
+ new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setUnderstandsButDescription(s.toString())));
+ recyclerView.setAdapter(adapter);
+ });
+
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
+ recyclerView.addItemDecoration(dividerItemDecoration);
+
+ ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> {
+ if (position > 0) {
+ mViewModel.removeLanguage(position - 1);
+ }
+ }, null));
+ itemTouchHelper.attachToRecyclerView(recyclerView);
+ }
+
+ private void setupAddLanguageButton(@NonNull FloatingActionButton fab) {
+ fab.setOnClickListener(view -> {
+ Language newLanguage = mViewModel.addNewLanguage();
+ navigateToEditLanguage(newLanguage);
+ });
+ }
+
+ private static class ViewHolder {
+ RecyclerView list;
+ FloatingActionButton addLanguage;
+
+ ViewHolder(@NonNull View root) {
+ this.list = root.findViewById(R.id.list);
+ this.addLanguage = root.findViewById(R.id.add_language);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java
new file mode 100644
index 0000000..e9163c6
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java
@@ -0,0 +1,114 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListHeaderBinding;
+import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListItemBinding;
+import com.majinnaibu.monstercards.models.Language;
+import com.majinnaibu.monstercards.ui.components.Stepper;
+
+import java.util.List;
+import java.util.Locale;
+
+public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter {
+ private final List mValues;
+ private final ItemCallback mOnClick;
+ private final int mTelepathyRange;
+ private final String mUnderstandsBut;
+ private final Stepper.OnValueChangeListener mOnTelepathyRangeChanged;
+ private final TextWatcher mOnUnderstandsButChanged;
+
+ private final int HEADER_VIEW_TYPE = 1;
+ private final int ITEM_VIEW_TYPE = 2;
+ private final String DISTANCE_IN_FEET_FORMAT = "%d ft.";
+
+ public EditLanguagesRecyclerViewAdapter(List items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String understandsBut, TextWatcher understandsButChangedListener) {
+ mValues = items;
+ mOnClick = onClick;
+ mTelepathyRange = telepathyRange;
+ mOnTelepathyRangeChanged = telepathyRangeChangedListener;
+ mUnderstandsBut = understandsBut;
+ mOnUnderstandsButChanged = understandsButChangedListener;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == HEADER_VIEW_TYPE) {
+ return new HeaderViewHolder(FragmentEditLanguagesListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+ return new ItemViewHolder(FragmentEditLanguagesListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof HeaderViewHolder) {
+ HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
+ headerViewHolder.telepathy.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), DISTANCE_IN_FEET_FORMAT, value));
+ headerViewHolder.telepathy.setValue(mTelepathyRange);
+ headerViewHolder.telepathy.setOnValueChangeListener(mOnTelepathyRangeChanged);
+ headerViewHolder.understandsBut.setText(mUnderstandsBut);
+ headerViewHolder.understandsBut.addTextChangedListener(mOnUnderstandsButChanged);
+ } else if (holder instanceof ItemViewHolder) {
+ ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
+ itemViewHolder.mItem = mValues.get(position - 1);
+ itemViewHolder.mContentView.setText(itemViewHolder.mItem.getName());
+ itemViewHolder.itemView.setOnClickListener(view -> {
+ if (mOnClick != null) {
+ mOnClick.onItemCallback(itemViewHolder.mItem);
+ }
+ });
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mValues.size() + 1;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0) {
+ return HEADER_VIEW_TYPE;
+ }
+ return ITEM_VIEW_TYPE;
+ }
+
+ public interface ItemCallback {
+ void onItemCallback(Language language);
+ }
+
+ public static class HeaderViewHolder extends RecyclerView.ViewHolder {
+ public final Stepper telepathy;
+ public final EditText understandsBut;
+
+ public HeaderViewHolder(@NonNull FragmentEditLanguagesListHeaderBinding binding) {
+ super(binding.getRoot());
+ telepathy = binding.telepathy;
+ understandsBut = binding.understandsBut;
+ }
+ }
+
+ public static class ItemViewHolder extends RecyclerView.ViewHolder {
+ public final TextView mContentView;
+ public Language mItem;
+
+ public ItemViewHolder(@NonNull FragmentEditLanguagesListItemBinding binding) {
+ super(binding.getRoot());
+ mContentView = binding.content;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return super.toString() + " '" + mContentView.getText() + "'";
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java
new file mode 100644
index 0000000..0e24067
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java
@@ -0,0 +1,272 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavBackStackEntry;
+import androidx.navigation.NavController;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.majinnaibu.monstercards.R;
+import com.majinnaibu.monstercards.data.MonsterRepository;
+import com.majinnaibu.monstercards.data.enums.StringType;
+import com.majinnaibu.monstercards.data.enums.TraitType;
+import com.majinnaibu.monstercards.models.Monster;
+import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentArgs;
+import com.majinnaibu.monstercards.ui.shared.MCFragment;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.util.Objects;
+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.schedulers.Schedulers;
+
+public class EditMonsterFragment extends MCFragment {
+
+ private EditMonsterViewModel mViewModel;
+ private ViewHolder mHolder;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+
+ MonsterRepository repository = getMonsterRepository();
+ Bundle arguments = getArguments();
+ assert arguments != null;
+ UUID monsterId = UUID.fromString(MonsterDetailFragmentArgs.fromBundle(arguments).getMonsterId());
+
+ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
+ NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
+ mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
+
+ View root = inflater.inflate(R.layout.fragment_edit_monster, container, false);
+ mHolder = new ViewHolder(root);
+
+ setTitle(getString(R.string.title_editMonster_fmt, getString(R.string.default_monster_name)));
+
+ // TODO: Show a loading spinner until we have the monster loaded.
+ if (mViewModel.hasError() || !mViewModel.hasLoaded() || !Objects.equals(mViewModel.getMonsterId().getValue(), monsterId)) {
+ repository.getMonster(monsterId).toObservable()
+ .firstOrError()
+ .subscribe(new DisposableSingleObserver() {
+ @Override
+ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) {
+ mViewModel.setHasLoaded(true);
+ mViewModel.setHasError(false);
+ mViewModel.copyFromMonster(monster);
+ setTitle(getString(R.string.title_editMonster_fmt, monster.name));
+ dispose();
+ }
+
+ @Override
+ public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
+ // TODO: Show an error state.
+ Logger.logError(e);
+ mViewModel.setHasError(true);
+ mViewModel.setErrorMessage(e.toString());
+ dispose();
+ }
+ });
+ }
+
+ mHolder.basicInfoButton.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.armorButton.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditArmorFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.speedButton.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSpeedFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.abilityScoresButton.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditAbilityScoresFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.savingThrows.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSavingThrowsFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.challengeRating.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditChallengeRatingFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.skills.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSkillsFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.senses.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.SENSE);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.conditionImmunities.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.CONDITION_IMMUNITY);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.damageImmunities.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_IMMUNITY);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.damageResistances.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_RESISTANCE);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.damageVulnerabilities.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_VULNERABILITY);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.languages.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment();
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.abilities.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ABILITY);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.actions.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ACTION);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.lairActions.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LAIR_ACTION);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.legendaryActions.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LEGENDARY_ACTION);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.reactions.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REACTIONS);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+ mHolder.regionalActions.setOnClickListener(v -> {
+ NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REGIONAL_ACTION);
+ Navigation.findNavController(requireView()).navigate(action);
+ });
+
+
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ if (mViewModel.hasChanges()) {
+ View view = getView();
+ AlertDialog alertDialog = new AlertDialog.Builder(requireContext()).create();
+ alertDialog.setTitle("Unsaved Changes");
+ alertDialog.setMessage("Do you want to save your changes?");
+ alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Save", (dialog, id) -> {
+ // Save the monster. Navigate up if the save is successful. Show a SnackBar if there was an error.
+ getMonsterRepository().saveMonster(mViewModel.buildMonster())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ new DisposableCompletableObserver() {
+ @Override
+ public void onComplete() {
+ Navigation.findNavController(requireView()).navigateUp();
+ }
+
+ @Override
+ public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
+ Logger.logError("Error creating monster", e);
+ assert view != null;
+ Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show();
+ }
+ });
+ });
+ alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> {
+ // Navigate up ignoring unsaved changes.
+ Navigation.findNavController(requireView()).navigateUp();
+ });
+ alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> {
+ // Do nothing.
+ });
+ alertDialog.show();
+ } else {
+ // No changes so we can safely leave.
+ Navigation.findNavController(requireView()).navigateUp();
+ }
+ }
+ });
+
+ return root;
+ }
+
+ private static class ViewHolder {
+
+ TextView basicInfoButton;
+ TextView armorButton;
+ TextView speedButton;
+ TextView abilityScoresButton;
+ TextView savingThrows;
+ TextView skills;
+ TextView conditionImmunities;
+ TextView damageImmunities;
+ TextView damageResistances;
+ TextView damageVulnerabilities;
+ TextView senses;
+ TextView languages;
+ TextView challengeRating;
+ TextView abilities;
+ TextView actions;
+ TextView reactions;
+ TextView legendaryActions;
+ TextView lairActions;
+ TextView regionalActions;
+
+ ViewHolder(@NonNull View root) {
+ basicInfoButton = root.findViewById(R.id.basicInfo);
+ armorButton = root.findViewById(R.id.armor);
+ speedButton = root.findViewById(R.id.speed);
+ abilityScoresButton = root.findViewById(R.id.abilityScores);
+ savingThrows = root.findViewById(R.id.savingThrows);
+ skills = root.findViewById(R.id.skills);
+ conditionImmunities = root.findViewById(R.id.conditionImmunities);
+ damageImmunities = root.findViewById(R.id.damageImmunities);
+ damageResistances = root.findViewById(R.id.damageResistances);
+ damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
+ senses = root.findViewById(R.id.senses);
+ languages = root.findViewById(R.id.languages);
+ challengeRating = root.findViewById(R.id.challengeRating);
+ abilities = root.findViewById(R.id.abilities);
+ actions = root.findViewById(R.id.actions);
+ reactions = root.findViewById(R.id.reactions);
+ legendaryActions = root.findViewById(R.id.legendaryActions);
+ lairActions = root.findViewById(R.id.lairActions);
+ regionalActions = root.findViewById(R.id.regionalActions);
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java
new file mode 100644
index 0000000..00f76f8
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java
@@ -0,0 +1,1092 @@
+package com.majinnaibu.monstercards.ui.editmonster;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+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.data.enums.StringType;
+import com.majinnaibu.monstercards.data.enums.TraitType;
+import com.majinnaibu.monstercards.helpers.StringHelper;
+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 com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
+import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
+import com.majinnaibu.monstercards.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+@SuppressWarnings({"ConstantConditions"})
+public class EditMonsterViewModel extends ChangeTrackedViewModel {
+ private final ChangeTrackedLiveData mMonsterId;
+ private final MutableLiveData mHasError;
+ private final MutableLiveData mHasLoaded;
+ private final ChangeTrackedLiveData mHasCustomHitPoints;
+ private final ChangeTrackedLiveData mHasShield;
+ private final ChangeTrackedLiveData mCanHover;
+ private final ChangeTrackedLiveData mHasCustomSpeed;
+ private final ChangeTrackedLiveData mArmorType;
+ private final ChangeTrackedLiveData mStrengthProficiency;
+ private final ChangeTrackedLiveData mStrengthAdvantage;
+ private final ChangeTrackedLiveData mDexterityProficiency;
+ private final ChangeTrackedLiveData mDexterityAdvantage;
+ private final ChangeTrackedLiveData mConstitutionProficiency;
+ private final ChangeTrackedLiveData mConstitutionAdvantage;
+ private final ChangeTrackedLiveData mIntelligenceProficiency;
+ private final ChangeTrackedLiveData mIntelligenceAdvantage;
+ private final ChangeTrackedLiveData mWisdomProficiency;
+ private final ChangeTrackedLiveData mWisdomAdvantage;
+ private final ChangeTrackedLiveData mCharismaProficiency;
+ private final ChangeTrackedLiveData mCharismaAdvantage;
+ private final ChangeTrackedLiveData mHitDice;
+ private final ChangeTrackedLiveData mNaturalArmorBonus;
+ private final ChangeTrackedLiveData mShieldBonus;
+ private final ChangeTrackedLiveData mWalkSpeed;
+ private final ChangeTrackedLiveData mBurrowSpeed;
+ private final ChangeTrackedLiveData mClimbSpeed;
+ private final ChangeTrackedLiveData mFlySpeed;
+ private final ChangeTrackedLiveData mSwimSpeed;
+ private final ChangeTrackedLiveData mStrength;
+ private final ChangeTrackedLiveData mDexterity;
+ private final ChangeTrackedLiveData mConstitution;
+ private final ChangeTrackedLiveData mIntelligence;
+ private final ChangeTrackedLiveData mWisdom;
+ private final ChangeTrackedLiveData mCharisma;
+ private final ChangeTrackedLiveData mName;
+ private final MutableLiveData mErrorMessage;
+ private final ChangeTrackedLiveData mSize;
+ private final ChangeTrackedLiveData mType;
+ private final ChangeTrackedLiveData mSubtype;
+ private final ChangeTrackedLiveData mAlignment;
+ private final ChangeTrackedLiveData mCustomHitPoints;
+ private final ChangeTrackedLiveData mCustomArmor;
+ private final ChangeTrackedLiveData mCustomSpeed;
+ private final ChangeTrackedLiveData mChallengeRating;
+ private final ChangeTrackedLiveData mCustomChallengeRatingDescription;
+ private final ChangeTrackedLiveData mCustomProficiencyBonus;
+ private final ChangeTrackedLiveData mTelepathyRange;
+ private final ChangeTrackedLiveData mUnderstandsButDescription;
+ private final ChangeTrackedLiveData> mSkills;
+ private final ChangeTrackedLiveData> mSenses;
+ private final ChangeTrackedLiveData> mDamageImmunities;
+ private final ChangeTrackedLiveData> mDamageResistances;
+ private final ChangeTrackedLiveData> mDamageVulnerabilities;
+ private final ChangeTrackedLiveData> mConditionImmunities;
+ private final ChangeTrackedLiveData> mLanguages;
+ private final ChangeTrackedLiveData> mAbilities;
+ private final ChangeTrackedLiveData> mActions;
+ private final ChangeTrackedLiveData> mReactions;
+ private final ChangeTrackedLiveData> mLairActions;
+ private final ChangeTrackedLiveData> mLegendaryActions;
+ private final ChangeTrackedLiveData> mRegionalActions;
+
+ public EditMonsterViewModel() {
+ super();
+ mErrorMessage = new MutableLiveData<>("");
+ mHasError = new MutableLiveData<>(false);
+ mHasLoaded = new MutableLiveData<>(false);
+
+ mName = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mMonsterId = new ChangeTrackedLiveData<>(UUID.randomUUID(), this::makeDirty);
+ mSize = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mType = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mSubtype = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mAlignment = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mCustomHitPoints = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mHitDice = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mNaturalArmorBonus = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mHasCustomHitPoints = new ChangeTrackedLiveData<>(false, this::makeDirty);
+ mArmorType = new ChangeTrackedLiveData<>(ArmorType.NONE, this::makeDirty);
+ mHasShield = new ChangeTrackedLiveData<>(false, this::makeDirty);
+ mShieldBonus = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mCustomArmor = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mWalkSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mBurrowSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mClimbSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mFlySpeed = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mSwimSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mCanHover = new ChangeTrackedLiveData<>(false, this::makeDirty);
+ mHasCustomSpeed = new ChangeTrackedLiveData<>(false, this::makeDirty);
+ mCustomSpeed = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mStrength = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mDexterity = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mConstitution = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mIntelligence = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mWisdom = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mCharisma = new ChangeTrackedLiveData<>(10, this::makeDirty);
+ mStrengthProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mStrengthAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mDexterityProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mDexterityAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mConstitutionProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mConstitutionAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mIntelligenceProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mIntelligenceAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mWisdomProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mWisdomAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mCharismaProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
+ mCharismaAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
+ mChallengeRating = new ChangeTrackedLiveData<>(ChallengeRating.ONE_EIGHTH, this::makeDirty);
+ mCustomChallengeRatingDescription = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mCustomProficiencyBonus = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mTelepathyRange = new ChangeTrackedLiveData<>(0, this::makeDirty);
+ mUnderstandsButDescription = new ChangeTrackedLiveData<>("", this::makeDirty);
+ mSkills = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mSenses = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mDamageImmunities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mDamageResistances = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mDamageVulnerabilities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mConditionImmunities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mLanguages = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mAbilities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mReactions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mLairActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mLegendaryActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ mRegionalActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
+ }
+
+ public void copyFromMonster(@NonNull Monster monster) {
+ mMonsterId.resetValue(monster.id);
+ mName.resetValue(monster.name);
+ mSize.resetValue(monster.size);
+ mType.resetValue(monster.type);
+ mSubtype.resetValue(monster.subtype);
+ mAlignment.resetValue(monster.alignment);
+ mCustomHitPoints.resetValue(monster.customHPDescription);
+ mHitDice.resetValue(monster.hitDice);
+ mNaturalArmorBonus.resetValue(monster.naturalArmorBonus);
+ mHasCustomHitPoints.resetValue(monster.hasCustomHP);
+ mArmorType.resetValue(monster.armorType);
+ mHasShield.resetValue(monster.shieldBonus != 0);
+ mShieldBonus.resetValue(monster.shieldBonus);
+ mCustomArmor.resetValue(monster.otherArmorDescription);
+ mWalkSpeed.resetValue(monster.walkSpeed);
+ mBurrowSpeed.resetValue(monster.burrowSpeed);
+ mClimbSpeed.resetValue(monster.climbSpeed);
+ mFlySpeed.resetValue(monster.flySpeed);
+ mSwimSpeed.resetValue(monster.swimSpeed);
+ mCanHover.resetValue(monster.canHover);
+ mHasCustomSpeed.resetValue(monster.hasCustomSpeed);
+ mCustomSpeed.resetValue(monster.customSpeedDescription);
+ mStrength.resetValue(monster.strengthScore);
+ mDexterity.resetValue(monster.dexterityScore);
+ mConstitution.resetValue(monster.constitutionScore);
+ mIntelligence.resetValue(monster.intelligenceScore);
+ mWisdom.resetValue(monster.wisdomScore);
+ mCharisma.resetValue(monster.charismaScore);
+ mStrengthProficiency.resetValue(monster.strengthSavingThrowProficiency);
+ mStrengthAdvantage.resetValue(monster.strengthSavingThrowAdvantage);
+ mDexterityProficiency.resetValue(monster.dexteritySavingThrowProficiency);
+ mDexterityAdvantage.resetValue(monster.dexteritySavingThrowAdvantage);
+ mConstitutionProficiency.resetValue(monster.constitutionSavingThrowProficiency);
+ mConstitutionAdvantage.resetValue(monster.constitutionSavingThrowAdvantage);
+ mIntelligenceProficiency.resetValue(monster.intelligenceSavingThrowProficiency);
+ mIntelligenceAdvantage.resetValue(monster.intelligenceSavingThrowAdvantage);
+ mWisdomProficiency.resetValue(monster.wisdomSavingThrowProficiency);
+ mWisdomAdvantage.resetValue(monster.wisdomSavingThrowAdvantage);
+ mCharismaProficiency.resetValue(monster.charismaSavingThrowProficiency);
+ mCharismaAdvantage.resetValue(monster.charismaSavingThrowAdvantage);
+ mChallengeRating.resetValue(monster.challengeRating);
+ mCustomChallengeRatingDescription.resetValue(monster.customChallengeRatingDescription);
+ mCustomProficiencyBonus.resetValue(monster.customProficiencyBonus);
+ mTelepathyRange.resetValue(monster.telepathyRange);
+ mUnderstandsButDescription.resetValue(monster.understandsButDescription);
+
+ ArrayList skills = new ArrayList<>(monster.skills);
+ Collections.sort(skills, Skill::compareTo);
+ mSkills.resetValue(skills);
+ ArrayList senses = new ArrayList<>(monster.senses);
+ Collections.sort(senses, String::compareToIgnoreCase);
+ mSenses.resetValue(senses);
+ ArrayList damageImmunities = new ArrayList<>(monster.damageImmunities);
+ Collections.sort(damageImmunities, String::compareToIgnoreCase);
+ mDamageImmunities.resetValue(damageImmunities);
+ ArrayList damageResistances = new ArrayList<>(monster.damageResistances);
+ Collections.sort(damageResistances, String::compareToIgnoreCase);
+ mDamageResistances.resetValue(damageResistances);
+ ArrayList damageVulnerabilities = new ArrayList<>(monster.damageVulnerabilities);
+ Collections.sort(damageVulnerabilities, String::compareToIgnoreCase);
+ mDamageVulnerabilities.resetValue(damageVulnerabilities);
+ ArrayList conditionImmunities = new ArrayList<>(monster.conditionImmunities);
+ Collections.sort(conditionImmunities, String::compareToIgnoreCase);
+ mConditionImmunities.resetValue(conditionImmunities);
+ ArrayList languages = new ArrayList<>(monster.languages);
+ Collections.sort(languages, Language::compareTo);
+ mLanguages.resetValue(languages);
+ mAbilities.resetValue(new ArrayList<>(monster.abilities));
+ mActions.resetValue(new ArrayList<>(monster.actions));
+ mReactions.resetValue(new ArrayList<>(monster.reactions));
+ mLairActions.resetValue(new ArrayList<>(monster.lairActions));
+ mLegendaryActions.resetValue(new ArrayList<>(monster.legendaryActions));
+ mRegionalActions.resetValue(new ArrayList<>(monster.regionalActions));
+ makeClean();
+ }
+
+ public LiveData getName() {
+ return mName;
+ }
+
+ public void setName(@NonNull String name) {
+ mName.setValue(name);
+ }
+
+ public LiveData getMonsterId() {
+ return mMonsterId;
+ }
+
+ public void setErrorMessage(@NonNull String errorMessage) {
+ mErrorMessage.setValue(errorMessage);
+ }
+
+ public LiveData getHasError() {
+ return mHasError;
+ }
+
+ public void setHasError(@NonNull Boolean hasError) {
+ mHasError.setValue(hasError);
+ }
+
+ public boolean hasError() {
+ return getHasError().getValue();
+ }
+
+ public LiveData getHasLoaded() {
+ return mHasLoaded;
+ }
+
+ public void setHasLoaded(@NonNull Boolean hasLoaded) {
+ mHasLoaded.setValue(hasLoaded);
+ }
+
+ public boolean hasLoaded() {
+ return getHasLoaded().getValue();
+ }
+
+ public LiveData getSize() {
+ return mSize;
+ }
+
+ public void setSize(@NonNull String size) {
+ mSize.setValue(size);
+ }
+
+ public LiveData getType() {
+ return mType;
+ }
+
+ public void setType(@NonNull String type) {
+ mType.setValue(type);
+ }
+
+ public LiveData getSubtype() {
+ return mSubtype;
+ }
+
+ public void setSubtype(@NonNull String subtype) {
+ mSubtype.setValue(subtype);
+ }
+
+ public LiveData getAlignment() {
+ return mAlignment;
+ }
+
+ public void setAlignment(@NonNull String alignment) {
+ mAlignment.setValue(alignment);
+ }
+
+ public LiveData getCustomHitPoints() {
+ return mCustomHitPoints;
+ }
+
+ public void setCustomHitPoints(String customHitPoints) {
+ mCustomHitPoints.setValue(customHitPoints);
+ }
+
+ public void setHitDice(int hitDice) {
+ mHitDice.setValue(hitDice);
+ }
+
+ public int getHitDiceUnboxed() {
+ return Helpers.unboxInteger(mHitDice.getValue(), 1);
+ }
+
+ public void setNaturalArmorBonus(int naturalArmorBonus) {
+ mNaturalArmorBonus.setValue(naturalArmorBonus);
+ }
+
+ public int getNaturalArmorBonusUnboxed() {
+ return Helpers.unboxInteger(mNaturalArmorBonus.getValue(), 0);
+ }
+
+ public LiveData getHasCustomHitPoints() {
+ return mHasCustomHitPoints;
+ }
+
+ public void setHasCustomHitPoints(boolean hasCustomHitPoints) {
+ mHasCustomHitPoints.setValue(hasCustomHitPoints);
+ }
+
+ public boolean getHasCustomHitPointsValueAsBoolean() {
+ return mHasCustomHitPoints.getValue();
+ }
+
+ public LiveData getArmorType() {
+ return mArmorType;
+ }
+
+ public void setArmorType(ArmorType armorType) {
+ mArmorType.setValue(armorType);
+ }
+
+ public void setHasShield(boolean hasShield) {
+ mHasShield.setValue(hasShield);
+ }
+
+ public boolean getHasShieldValueAsBoolean() {
+ return mHasShield.getValue();
+ }
+
+ public void setShieldBonus(int shieldBonus) {
+ mShieldBonus.setValue(shieldBonus);
+ }
+
+ public int getShieldBonusUnboxed() {
+ return Helpers.unboxInteger(mShieldBonus.getValue(), 0);
+ }
+
+ public LiveData getCustomArmor() {
+ return mCustomArmor;
+ }
+
+ public void setCustomArmor(String customArmor) {
+ mCustomArmor.setValue(customArmor);
+ }
+
+ public String getShieldBonusValueAsString() {
+ return mShieldBonus.getValue().toString();
+ }
+
+ public LiveData getWalkSpeed() {
+ return mWalkSpeed;
+ }
+
+ public void setWalkSpeed(int walkSpeed) {
+ mWalkSpeed.setValue(walkSpeed);
+ }
+
+ public LiveData getBurrowSpeed() {
+ return mBurrowSpeed;
+ }
+
+ public void setBurrowSpeed(int burrowSpeed) {
+ mBurrowSpeed.setValue(burrowSpeed);
+ }
+
+ public LiveData getClimbSpeed() {
+ return mClimbSpeed;
+ }
+
+ public void setClimbSpeed(int climbSpeed) {
+ mClimbSpeed.setValue(climbSpeed);
+ }
+
+ public LiveData getFlySpeed() {
+ return mFlySpeed;
+ }
+
+ public void setFlySpeed(int flySpeed) {
+ mFlySpeed.setValue(flySpeed);
+ }
+
+ public LiveData getSwimSpeed() {
+ return mSwimSpeed;
+ }
+
+ public void setSwimSpeed(int swimSpeed) {
+ mSwimSpeed.setValue(swimSpeed);
+ }
+
+ public LiveData getCanHover() {
+ return mCanHover;
+ }
+
+ public void setCanHover(boolean canHover) {
+ mCanHover.setValue(canHover);
+ }
+
+ public LiveData getHasCustomSpeed() {
+ return mHasCustomSpeed;
+ }
+
+ public void setHasCustomSpeed(boolean hasCustomSpeed) {
+ mHasCustomSpeed.setValue(hasCustomSpeed);
+ }
+
+ public LiveData getCustomSpeed() {
+ return mCustomSpeed;
+ }
+
+ public void setCustomSpeed(String customSpeed) {
+ mCustomSpeed.setValue(customSpeed);
+ }
+
+ public LiveData getStrength() {
+ return mStrength;
+ }
+
+ public void setStrength(int strength) {
+ mStrength.setValue(strength);
+ }
+
+ public LiveData getDexterity() {
+ return mDexterity;
+ }
+
+ public void setDexterity(int dexterity) {
+ mDexterity.setValue(dexterity);
+ }
+
+ public LiveData getConstitution() {
+ return mConstitution;
+ }
+
+ public void setConstitution(int constitution) {
+ mConstitution.setValue(constitution);
+ }
+
+ public LiveData getIntelligence() {
+ return mIntelligence;
+ }
+
+ public void setIntelligence(int intelligence) {
+ mIntelligence.setValue(intelligence);
+ }
+
+ public LiveData getWisdom() {
+ return mWisdom;
+ }
+
+ public void setWisdom(int wisdom) {
+ mWisdom.setValue(wisdom);
+ }
+
+ public LiveData getCharisma() {
+ return mCharisma;
+ }
+
+ public void setCharisma(int charisma) {
+ mCharisma.setValue(charisma);
+ }
+
+ public LiveData getStrengthProficiency() {
+ return mStrengthProficiency;
+ }
+
+ public void setStrengthProficiency(ProficiencyType proficiency) {
+ mStrengthProficiency.setValue(proficiency);
+ }
+
+ public LiveData getStrengthAdvantage() {
+ return mStrengthAdvantage;
+ }
+
+ public void setStrengthAdvantage(AdvantageType advantage) {
+ mStrengthAdvantage.setValue(advantage);
+ }
+
+ public LiveData getDexterityProficiency() {
+ return mDexterityProficiency;
+ }
+
+ public void setDexterityProficiency(ProficiencyType proficiency) {
+ mDexterityProficiency.setValue(proficiency);
+ }
+
+ public LiveData getDexterityAdvantage() {
+ return mDexterityAdvantage;
+ }
+
+ public void setDexterityAdvantage(AdvantageType advantage) {
+ mDexterityAdvantage.setValue(advantage);
+ }
+
+ public LiveData getConstitutionProficiency() {
+ return mConstitutionProficiency;
+ }
+
+ public void setConstitutionProficiency(ProficiencyType proficiency) {
+ mConstitutionProficiency.setValue(proficiency);
+ }
+
+ public LiveData getConstitutionAdvantage() {
+ return mConstitutionAdvantage;
+ }
+
+ public void setConstitutionAdvantage(AdvantageType advantage) {
+ mConstitutionAdvantage.setValue(advantage);
+ }
+
+ public LiveData getIntelligenceProficiency() {
+ return mIntelligenceProficiency;
+ }
+
+ public void setIntelligenceProficiency(ProficiencyType proficiency) {
+ mIntelligenceProficiency.setValue(proficiency);
+ }
+
+ public LiveData getIntelligenceAdvantage() {
+ return mIntelligenceAdvantage;
+ }
+
+ public void setIntelligenceAdvantage(AdvantageType advantage) {
+ mIntelligenceAdvantage.setValue(advantage);
+ }
+
+ public LiveData getWisdomProficiency() {
+ return mWisdomProficiency;
+ }
+
+ public void setWisdomProficiency(ProficiencyType proficiency) {
+ mWisdomProficiency.setValue(proficiency);
+ }
+
+ public LiveData getWisdomAdvantage() {
+ return mWisdomAdvantage;
+ }
+
+ public void setWisdomAdvantage(AdvantageType advantage) {
+ mWisdomAdvantage.setValue(advantage);
+ }
+
+ public LiveData getCharismaProficiency() {
+ return mCharismaProficiency;
+ }
+
+ public void setCharismaProficiency(ProficiencyType proficiency) {
+ mCharismaProficiency.setValue(proficiency);
+ }
+
+ public LiveData getCharismaAdvantage() {
+ return mCharismaAdvantage;
+ }
+
+ public void setCharismaAdvantage(AdvantageType advantage) {
+ mCharismaAdvantage.setValue(advantage);
+ }
+
+ public LiveData getChallengeRating() {
+ return mChallengeRating;
+ }
+
+ public void setChallengeRating(ChallengeRating challengeRating) {
+ mChallengeRating.setValue(challengeRating);
+ }
+
+ public LiveData getCustomChallengeRatingDescription() {
+ return mCustomChallengeRatingDescription;
+ }
+
+ public void setCustomChallengeRatingDescription(String customChallengeRatingDescription) {
+ mCustomChallengeRatingDescription.setValue(customChallengeRatingDescription);
+ }
+
+ public LiveData getCustomProficiencyBonus() {
+ return mCustomProficiencyBonus;
+ }
+
+ public void setCustomProficiencyBonus(int proficiencyBonus) {
+ mCustomProficiencyBonus.setValue(proficiencyBonus);
+ }
+
+ public void setCustomProficiencyBonus(String proficiencyBonus) {
+ Integer parsedValue = StringHelper.parseInt(proficiencyBonus);
+ this.setCustomProficiencyBonus(parsedValue != null ? parsedValue : 0);
+ }
+
+ public String getCustomProficiencyBonusValueAsString() {
+ return mCustomProficiencyBonus.getValue().toString();
+ }
+
+ public LiveData getTelepathyRange() {
+ return mTelepathyRange;
+ }
+
+ public void setTelepathyRange(int telepathyRange) {
+ mTelepathyRange.setValue(telepathyRange);
+ }
+
+ public int getTelepathyRangeUnboxed() {
+ return Helpers.unboxInteger(mTelepathyRange.getValue(), 0);
+ }
+
+ public LiveData getUnderstandsButDescription() {
+ return mUnderstandsButDescription;
+ }
+
+ public void setUnderstandsButDescription(String understandsButDescription) {
+ mUnderstandsButDescription.setValue(understandsButDescription);
+ }
+
+ public LiveData> getSkills() {
+ return mSkills;
+ }
+
+ public List getSkillsArray() {
+ return mSkills.getValue();
+ }
+
+ public Skill addNewSkill() {
+ Skill newSkill = new Skill("Unnamed Skill", AbilityScore.DEXTERITY);
+ ArrayList newSkills = new ArrayList<>(mSkills.getValue());
+ newSkills.add(newSkill);
+ Collections.sort(newSkills, (skill1, skill2) -> skill1.name.compareToIgnoreCase(skill2.name));
+ mSkills.setValue(newSkills);
+ return newSkill;
+ }
+
+ public void removeSkill(int position) {
+ List skills = mSkills.getValue();
+ ArrayList newSkills = new ArrayList<>(skills);
+ newSkills.remove(position);
+ mSkills.setValue(newSkills);
+ }
+
+ public void replaceSkill(Skill newSkill, Skill oldSkill) {
+ List oldSkills = mSkills.getValue();
+ if (oldSkills == null) {
+ oldSkills = new ArrayList<>();
+ }
+ boolean hasReplaced = false;
+ ArrayList newSkills = new ArrayList<>(oldSkills.size());
+ for (Skill skill : oldSkills) {
+ if (Objects.equals(skill, oldSkill)) {
+ newSkills.add(newSkill);
+ hasReplaced = true;
+ } else {
+ newSkills.add(skill);
+ }
+ }
+ if (!hasReplaced) {
+ newSkills.add(newSkill);
+ }
+ Collections.sort(newSkills, (skill1, skill2) -> skill1.name.compareToIgnoreCase(skill2.name));
+ mSkills.setValue(newSkills);
+ }
+
+ public LiveData> getSenses() {
+ return mSenses;
+ }
+
+ public LiveData> getDamageImmunities() {
+ return mDamageImmunities;
+ }
+
+ public List getDamageImmunitiesArray() {
+ return mDamageImmunities.getValue();
+ }
+
+ public LiveData> getDamageResistances() {
+ return mDamageResistances;
+ }
+
+ public LiveData> getLanguages() {
+ return mLanguages;
+ }
+
+ public List getLanguagesArray() {
+ return mLanguages.getValue();
+ }
+
+ public Language addNewLanguage() {
+ Language newLanguage = new Language("", true);
+ return Helpers.addItemToList(mLanguages, newLanguage, Language::compareTo);
+ }
+
+ public void removeLanguage(int position) {
+ Helpers.removeFromList(mLanguages, position);
+ }
+
+ public void replaceLanguage(Language oldLanguage, Language newLanguage) {
+ Helpers.replaceItemInList(mLanguages, oldLanguage, newLanguage, Language::compareTo);
+ }
+
+ public Monster buildMonster() {
+ Monster monster = new Monster();
+
+ monster.id = mMonsterId.getValue();
+ monster.name = mName.getValue();
+ monster.size = mSize.getValue();
+ monster.type = mType.getValue();
+ monster.subtype = mSubtype.getValue();
+ monster.alignment = mAlignment.getValue();
+ monster.customHPDescription = mCustomHitPoints.getValue();
+ monster.hitDice = mHitDice.getValue();
+ monster.hasCustomHP = mHasCustomHitPoints.getValue();
+ monster.armorType = mArmorType.getValue();
+ monster.naturalArmorBonus = mNaturalArmorBonus.getValue();
+ monster.shieldBonus = mShieldBonus.getValue();
+ monster.otherArmorDescription = mCustomArmor.getValue();
+ monster.walkSpeed = mWalkSpeed.getValue();
+ monster.burrowSpeed = mBurrowSpeed.getValue();
+ monster.climbSpeed = mClimbSpeed.getValue();
+ monster.flySpeed = mFlySpeed.getValue();
+ monster.swimSpeed = mSwimSpeed.getValue();
+ monster.canHover = mCanHover.getValue();
+ monster.hasCustomSpeed = mHasCustomSpeed.getValue();
+ monster.customSpeedDescription = mCustomSpeed.getValue();
+ monster.strengthScore = mStrength.getValue();
+ monster.dexterityScore = mDexterity.getValue();
+ monster.constitutionScore = mConstitution.getValue();
+ monster.intelligenceScore = mIntelligence.getValue();
+ monster.wisdomScore = mWisdom.getValue();
+ monster.charismaScore = mCharisma.getValue();
+ monster.strengthSavingThrowAdvantage = mStrengthAdvantage.getValue();
+ monster.strengthSavingThrowProficiency = mStrengthProficiency.getValue();
+ monster.dexteritySavingThrowAdvantage = mDexterityAdvantage.getValue();
+ monster.dexteritySavingThrowProficiency = mDexterityProficiency.getValue();
+ monster.constitutionSavingThrowAdvantage = mConstitutionAdvantage.getValue();
+ monster.constitutionSavingThrowProficiency = mConstitutionProficiency.getValue();
+ monster.intelligenceSavingThrowAdvantage = mIntelligenceAdvantage.getValue();
+ monster.intelligenceSavingThrowProficiency = mIntelligenceProficiency.getValue();
+ monster.wisdomSavingThrowAdvantage = mWisdomAdvantage.getValue();
+ monster.wisdomSavingThrowProficiency = mWisdomProficiency.getValue();
+ monster.charismaSavingThrowAdvantage = mCharismaAdvantage.getValue();
+ monster.charismaSavingThrowProficiency = mCharismaProficiency.getValue();
+ monster.challengeRating = mChallengeRating.getValue();
+ monster.customChallengeRatingDescription = mCustomChallengeRatingDescription.getValue();
+ monster.customProficiencyBonus = mCustomProficiencyBonus.getValue();
+ monster.telepathyRange = mTelepathyRange.getValue();
+ monster.understandsButDescription = mUnderstandsButDescription.getValue();
+ monster.skills = new HashSet<>(mSkills.getValue());
+ monster.senses = new HashSet<>(mSenses.getValue());
+ monster.damageImmunities = new HashSet<>(mDamageImmunities.getValue());
+ monster.damageResistances = new HashSet<>(mDamageResistances.getValue());
+ monster.damageVulnerabilities = new HashSet<>(mDamageVulnerabilities.getValue());
+ monster.conditionImmunities = new HashSet<>(mConditionImmunities.getValue());
+ monster.languages = new HashSet<>(mLanguages.getValue());
+ monster.abilities = new ArrayList<>(mAbilities.getValue());
+ monster.actions = new ArrayList<>(mActions.getValue());
+ monster.reactions = new ArrayList<>(mReactions.getValue());
+ monster.lairActions = new ArrayList<>(mLairActions.getValue());
+ monster.legendaryActions = new ArrayList<>(mLegendaryActions.getValue());
+ monster.regionalActions = new ArrayList<>(mRegionalActions.getValue());
+
+ return monster;
+ }
+
+ public LiveData> getTraits(@NonNull TraitType type) {
+ switch (type) {
+ case ABILITY:
+ return mAbilities;
+ case ACTION:
+ return mActions;
+ case LAIR_ACTION:
+ return mLairActions;
+ case LEGENDARY_ACTION:
+ return mLegendaryActions;
+ case REACTIONS:
+ return mReactions;
+ case REGIONAL_ACTION:
+ return mRegionalActions;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type));
+ return null;
+ }
+ }
+
+ public void removeTrait(@NonNull TraitType type, int position) {
+ switch (type) {
+ case ABILITY:
+ Helpers.removeFromList(mAbilities, position);
+ break;
+ case ACTION:
+ Helpers.removeFromList(mActions, position);
+ break;
+ case LAIR_ACTION:
+ Helpers.removeFromList(mLairActions, position);
+ break;
+ case LEGENDARY_ACTION:
+ Helpers.removeFromList(mLegendaryActions, position);
+ break;
+ case REACTIONS:
+ Helpers.removeFromList(mReactions, position);
+ break;
+ case REGIONAL_ACTION:
+ Helpers.removeFromList(mRegionalActions, position);
+ break;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type));
+ break;
+ }
+ }
+
+ public void replaceTrait(@NonNull TraitType type, Trait oldTrait, Trait newTrait) {
+ switch (type) {
+ case ABILITY:
+ Helpers.replaceItemInList(mAbilities, oldTrait, newTrait);
+ break;
+ case ACTION:
+ Helpers.replaceItemInList(mActions, oldTrait, newTrait);
+ break;
+ case LAIR_ACTION:
+ Helpers.replaceItemInList(mLairActions, oldTrait, newTrait);
+ break;
+ case LEGENDARY_ACTION:
+ Helpers.replaceItemInList(mLegendaryActions, oldTrait, newTrait);
+ break;
+ case REACTIONS:
+ Helpers.replaceItemInList(mReactions, oldTrait, newTrait);
+ break;
+ case REGIONAL_ACTION:
+ Helpers.replaceItemInList(mRegionalActions, oldTrait, newTrait);
+ break;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type));
+ }
+ }
+
+ public Trait addNewTrait(@NonNull TraitType type) {
+ Trait newAction = new Trait("", "");
+ switch (type) {
+ case ABILITY:
+ return Helpers.addItemToList(mAbilities, newAction);
+ case ACTION:
+ return Helpers.addItemToList(mActions, newAction);
+ case LAIR_ACTION:
+ return Helpers.addItemToList(mLairActions, newAction);
+ case LEGENDARY_ACTION:
+ return Helpers.addItemToList(mLegendaryActions, newAction);
+ case REACTIONS:
+ return Helpers.addItemToList(mReactions, newAction);
+ case REGIONAL_ACTION:
+ return Helpers.addItemToList(mRegionalActions, newAction);
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type));
+ return null;
+ }
+ }
+
+ public LiveData> getStrings(@NonNull StringType type) {
+ switch (type) {
+ case CONDITION_IMMUNITY:
+ return mConditionImmunities;
+ case DAMAGE_IMMUNITY:
+ return mDamageImmunities;
+ case DAMAGE_RESISTANCE:
+ return mDamageResistances;
+ case DAMAGE_VULNERABILITY:
+ return mDamageVulnerabilities;
+ case SENSE:
+ return mSenses;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type));
+ return null;
+ }
+ }
+
+ public void removeString(@NonNull StringType type, int position) {
+ switch (type) {
+ case CONDITION_IMMUNITY:
+ Helpers.removeFromList(mConditionImmunities, position);
+ break;
+ case DAMAGE_IMMUNITY:
+ Helpers.removeFromList(mDamageImmunities, position);
+ break;
+ case DAMAGE_RESISTANCE:
+ Helpers.removeFromList(mDamageResistances, position);
+ break;
+ case DAMAGE_VULNERABILITY:
+ Helpers.removeFromList(mDamageVulnerabilities, position);
+ break;
+ case SENSE:
+ Helpers.removeFromList(mSenses, position);
+ break;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type));
+ break;
+ }
+ }
+
+ public String addNewString(@NonNull StringType type) {
+ String newString = "";
+ switch (type) {
+ case CONDITION_IMMUNITY:
+ return Helpers.addItemToList(mConditionImmunities, newString);
+ case DAMAGE_IMMUNITY:
+ return Helpers.addItemToList(mDamageImmunities, newString);
+ case DAMAGE_RESISTANCE:
+ return Helpers.addItemToList(mDamageResistances, newString);
+ case DAMAGE_VULNERABILITY:
+ return Helpers.addItemToList(mDamageVulnerabilities, newString);
+ case SENSE:
+ return Helpers.addItemToList(mSenses, newString);
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type));
+ return null;
+ }
+ }
+
+ public void replaceString(@NonNull StringType type, String oldValue, String newValue) {
+ switch (type) {
+ case CONDITION_IMMUNITY:
+ Helpers.replaceItemInList(mConditionImmunities, oldValue, newValue);
+ break;
+ case DAMAGE_IMMUNITY:
+ Helpers.replaceItemInList(mDamageImmunities, oldValue, newValue);
+ break;
+ case DAMAGE_RESISTANCE:
+ Helpers.replaceItemInList(mDamageResistances, oldValue, newValue);
+ break;
+ case DAMAGE_VULNERABILITY:
+ Helpers.replaceItemInList(mDamageVulnerabilities, oldValue, newValue);
+ break;
+ case SENSE:
+ Helpers.replaceItemInList(mSenses, oldValue, newValue);
+ break;
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type));
+ }
+ }
+
+ public boolean moveTrait(@NonNull TraitType type, int from, int to) {
+ switch (type) {
+ case ABILITY:
+ return Helpers.moveItemInList(mAbilities, from, to);
+ case ACTION:
+ return Helpers.moveItemInList(mActions, from, to);
+ case LAIR_ACTION:
+ return Helpers.moveItemInList(mLairActions, from, to);
+ case LEGENDARY_ACTION:
+ return Helpers.moveItemInList(mLegendaryActions, from, to);
+ case REACTIONS:
+ return Helpers.moveItemInList(mReactions, from, to);
+ case REGIONAL_ACTION:
+ return Helpers.moveItemInList(mRegionalActions, from, to);
+ default:
+ Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type));
+ return false;
+ }
+
+
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static class Helpers {
+ static T addItemToList(MutableLiveData> listData, T newItem) {
+ return addItemToList(listData, newItem, null);
+ }
+
+ static T addItemToList(@NonNull MutableLiveData> listData, T newItem, Comparator super T> comparator) {
+ ArrayList newList = new ArrayList<>(listData.getValue());
+ newList.add(newItem);
+ if (comparator != null) {
+ Collections.sort(newList, comparator);
+ }
+ listData.setValue(newList);
+ return newItem;
+ }
+
+ static void removeFromList(@NonNull MutableLiveData> listData, int position) {
+ List oldList = listData.getValue();
+ ArrayList newList = new ArrayList<>(oldList);
+ newList.remove(position);
+ listData.setValue(newList);
+ }
+
+ static void replaceItemInList(@NonNull MutableLiveData> listData, int position, T newItem, Comparator super T> comparator) {
+ List oldList = listData.getValue();
+ if (oldList == null) {
+ oldList = new ArrayList<>();
+ }
+ int size = oldList.size();
+ boolean hasReplaced = false;
+ ArrayList newList = new ArrayList<>(size);
+ for (int index = 0; index < size; index++) {
+ if (index == position) {
+ newList.add(newItem);
+ hasReplaced = true;
+ } else {
+ newList.add(oldList.get(index));
+ }
+ }
+ if (!hasReplaced) {
+ newList.add(newItem);
+ }
+ if (comparator != null) {
+ Collections.sort(newList, comparator);
+ }
+ listData.setValue(newList);
+ }
+
+ @SuppressWarnings("unused")
+ static void replaceItemInList(MutableLiveData> listData, int position, T newItem) {
+ replaceItemInList(listData, position, newItem, null);
+ }
+
+ static void replaceItemInList(@NonNull MutableLiveData> listData, T oldItem, T newItem, Comparator super T> comparator) {
+ List oldList = listData.getValue();
+ if (oldList == null) {
+ oldList = new ArrayList<>();
+ }
+ boolean hasReplaced = false;
+ ArrayList newList = new ArrayList<>(oldList.size());
+ for (T item : oldList) {
+ if (!hasReplaced && Objects.equals(item, oldItem)) {
+ newList.add(newItem);
+ hasReplaced = true;
+ } else {
+ newList.add(item);
+ }
+ }
+ if (!hasReplaced) {
+ newList.add(newItem);
+ }
+ if (comparator != null) {
+ Collections.sort(newList, comparator);
+ }
+ listData.setValue(newList);
+ }
+
+ static void replaceItemInList(MutableLiveData> listData, T oldItem, T newItem) {
+ replaceItemInList(listData, oldItem, newItem, null);
+ }
+
+ static int unboxInteger(Integer value, int defaultIfNull) {
+ if (value == null) {
+ return defaultIfNull;
+ }
+ return value;
+ }
+
+ static boolean moveItemInList(@NonNull ChangeTrackedLiveData> listData, int from, int to) {
+ List