diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
index 7cd00ee..fc0f333 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
@@ -28,6 +28,8 @@ import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.importers.BinderImporter;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.init.AppCenterInitializer;
+import com.majinnaibu.monstercards.models.BinderExport;
+import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.ToastHelper;
@@ -35,6 +37,8 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -131,46 +135,161 @@ public class MainActivity extends AppCompatActivity {
}
}
+ Uri intentUri = getUriFromIntent(intent);
+ String fileName = intentUri != null ? getFileNameFromUri(intentUri) : null;
String json = readMonsterJSONFromIntent(intent);
if (!StringHelper.isNullOrEmpty(json)) {
- importMonsterFromInputAndNavigate(json);
+ importMonsterFromInputAndNavigate(json, fileName);
}
}
+ @Nullable
+ private Uri getUriFromIntent(@NonNull Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ if ("android.intent.action.SEND".equals(action)) {
+ if (extras != null) {
+ return extras.getParcelable(Intent.EXTRA_STREAM);
+ }
+ } else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
+ return intent.getData();
+ }
+ return null;
+ }
+
public void importMonsterFromInputAndNavigate(@NonNull String input) {
- BinderImporter binderImporter = new BinderImporter();
- if (binderImporter.canImport(input)) {
+ importMonsterFromInputAndNavigate(input, null);
+ }
+
+ public void importMonsterFromInputAndNavigate(@NonNull String input, @Nullable String fileName) {
+ try {
+ BinderImporter binderImporter = new BinderImporter();
+ if (binderImporter.canImport(input)) {
+ ToastHelper.showShort(this, R.string.toast_importing_url);
+ Single.fromCallable(() -> binderImporter.parse(input))
+ .flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ ToastHelper.showLong(this, R.string.snackbar_import_binder_success);
+ NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
+ NavController navController = navHostFragment.getNavController();
+ navController.navigate(R.id.navigation_library);
+ }, throwable -> {
+ Logger.logError("Failed to import binder from input: " + fileName, throwable);
+ ToastHelper.showLong(this, R.string.failed_to_import_url);
+ });
+ return;
+ }
+
ToastHelper.showShort(this, R.string.toast_importing_url);
- Single.fromCallable(() -> binderImporter.parse(input))
- .flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
+ Single.fromCallable(() -> MonsterImportHelper.fromJSON(input, fileName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> {
- ToastHelper.showLong(this, R.string.snackbar_import_binder_success);
+ .subscribe(monster -> {
+ String serializedJson = new Gson().toJson(monster);
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController();
- navController.navigate(R.id.navigation_library);
+ NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
+ navController.navigate(navAction);
}, throwable -> {
- Logger.logError("Failed to import binder from input", throwable);
+ Logger.logError("Failed to import monster from input: " + fileName, throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url);
});
+ } catch (Exception e) {
+ Logger.logError("Failed to process import input: " + fileName, e);
+ ToastHelper.showLong(this, R.string.failed_to_import_url);
+ }
+ }
+
+ public void importMultipleFilesFromUris(@NonNull List uris) {
+ if (uris.isEmpty()) return;
+ if (uris.size() == 1) {
+ Uri singleUri = uris.get(0);
+ String fileName = getFileNameFromUri(singleUri);
+ String content = readContentsOfUri(singleUri);
+ if (content != null && !content.trim().isEmpty()) {
+ importMonsterFromInputAndNavigate(content, fileName);
+ } else {
+ ToastHelper.showLong(this, R.string.failed_to_import_url);
+ }
return;
}
ToastHelper.showShort(this, R.string.toast_importing_url);
- Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(monster -> {
- String serializedJson = new Gson().toJson(monster);
- NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
- NavController navController = navHostFragment.getNavController();
- NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
- navController.navigate(navAction);
- }, throwable -> {
- Logger.logError("Failed to import monster from input", throwable);
- ToastHelper.showLong(this, R.string.failed_to_import_url);
- });
+ Single.fromCallable(() -> {
+ int monstersImported = 0;
+ int bindersImported = 0;
+
+ for (Uri uri : uris) {
+ try {
+ String fileName = getFileNameFromUri(uri);
+ String content = readContentsOfUri(uri);
+ if (content == null || content.trim().isEmpty()) {
+ continue;
+ }
+
+ BinderImporter binderImporter = new BinderImporter();
+ if (binderImporter.canImport(content)) {
+ BinderExport binder = binderImporter.parse(content);
+ ((MonsterCardsApplication) getApplication()).getMonsterRepository()
+ .importBinder(binder)
+ .blockingAwait();
+ bindersImported++;
+ } else {
+ Monster monster = MonsterImportHelper.fromJSON(content, fileName);
+ ((MonsterCardsApplication) getApplication()).getMonsterRepository()
+ .saveMonster(monster)
+ .blockingAwait();
+ monstersImported++;
+ }
+ } catch (Exception e) {
+ Logger.logError("Failed to import file URI: " + uri, e);
+ }
+ }
+
+ return new BatchImportResult(monstersImported, bindersImported);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ if (result.bindersImported > 0 || result.monstersImported > 0) {
+ String message = buildImportSummaryMessage(result.monstersImported, result.bindersImported);
+ ToastHelper.showLong(this, message);
+ NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
+ NavController navController = navHostFragment.getNavController();
+ navController.navigate(R.id.navigation_library);
+ } else {
+ ToastHelper.showLong(this, R.string.failed_to_import_url);
+ }
+ }, throwable -> {
+ Logger.logError("Failed to execute batch file import", throwable);
+ ToastHelper.showLong(this, R.string.failed_to_import_url);
+ });
+ }
+
+ private String buildImportSummaryMessage(int monstersCount, int bindersCount) {
+ StringBuilder sb = new StringBuilder("Successfully imported ");
+ if (bindersCount > 0 && monstersCount > 0) {
+ sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections")
+ .append(" and ")
+ .append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
+ } else if (bindersCount > 0) {
+ sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections");
+ } else {
+ sb.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
+ }
+ return sb.toString();
+ }
+
+ private static class BatchImportResult {
+ final int monstersImported;
+ final int bindersImported;
+
+ BatchImportResult(int monstersImported, int bindersImported) {
+ this.monstersImported = monstersImported;
+ this.bindersImported = bindersImported;
+ }
}
@Nullable
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java
index c687649..fd95fbc 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java
@@ -47,9 +47,18 @@ public interface CollectionDAO {
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
Completable removeMonsterFromCollection(String collectionId, String monsterId);
+ @Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId")
+ Completable removeAllMonstersFromCollection(String collectionId);
+
@Query("DELETE FROM collection_monsters WHERE id = :id")
Completable removeCollectionMonsterById(long id);
+ @Query("DELETE FROM collection_monsters")
+ Completable deleteAllCollectionMonsters();
+
+ @Query("DELETE FROM collections")
+ Completable deleteAllCollections();
+
@Update
Completable updateCollectionMonsters(List collectionMonsters);
}
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
index 9671790..6ccba4c 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java
@@ -36,4 +36,7 @@ public interface MonsterDAO {
@Delete
Completable delete(Monster monster);
+
+ @Query("DELETE FROM monsters")
+ Completable deleteAllMonsters();
}
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
index 5acb685..b58a161 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java
@@ -9,18 +9,22 @@ import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.DashboardMonster;
+import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
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.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
@SuppressWarnings("ResultOfMethodCallIgnored")
@@ -264,6 +268,91 @@ public class MonsterRepository {
return result;
}
+ public Completable clearAllData() {
+ return Completable.fromAction(() -> {
+ m_db.dashboardDAO().clearDashboard().blockingAwait();
+ m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
+ m_db.collectionDAO().deleteAllCollections().blockingAwait();
+ m_db.monsterDAO().deleteAllMonsters().blockingAwait();
+ }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Completable removeAllCollections() {
+ return Completable.fromAction(() -> {
+ m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
+ m_db.collectionDAO().deleteAllCollections().blockingAwait();
+ }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Completable removeAllMonstersFromCollection(@NonNull UUID collectionId) {
+ Completable result = m_db.collectionDAO().removeAllMonstersFromCollection(collectionId.toString());
+ result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ return result;
+ }
+
+ public Single exportCollections() {
+ return m_db.collectionDAO().getAll().first(new ArrayList<>())
+ .map(collections -> {
+ List colExports = new ArrayList<>();
+ for (Collection col : collections) {
+ List colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
+ .first(new ArrayList<>()).blockingGet();
+ colExports.add(new BinderExport.CollectionExport(
+ col.id.toString(),
+ col.name,
+ col.description,
+ colMonsters
+ ));
+ }
+ return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, null);
+ })
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public Single exportEverything() {
+ return Single.zip(
+ m_db.collectionDAO().getAll().first(new ArrayList<>()),
+ m_db.monsterDAO().getAll().first(new ArrayList<>()),
+ m_db.dashboardDAO().getDashboardMonsters().first(new ArrayList<>()),
+ (collections, allMonsters, dashboardMonsters) -> {
+ List colExports = new ArrayList<>();
+ Set monstersInCollections = new HashSet<>();
+
+ for (Collection col : collections) {
+ List colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
+ .first(new ArrayList<>()).blockingGet();
+ for (Monster m : colMonsters) {
+ monstersInCollections.add(m.id);
+ }
+ colExports.add(new BinderExport.CollectionExport(
+ col.id.toString(),
+ col.name,
+ col.description,
+ colMonsters
+ ));
+ }
+
+ List uncategorized = new ArrayList<>();
+ for (Monster m : allMonsters) {
+ if (!monstersInCollections.contains(m.id)) {
+ uncategorized.add(m);
+ }
+ }
+
+ if (!uncategorized.isEmpty() || colExports.isEmpty()) {
+ colExports.add(new BinderExport.CollectionExport(
+ UUID.randomUUID().toString(),
+ colExports.isEmpty() ? "All Monsters" : "Uncategorized",
+ "",
+ colExports.isEmpty() ? allMonsters : uncategorized
+ ));
+ }
+
+ return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, dashboardMonsters);
+ }
+ ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
public Completable updateDashboardMonsters(List items) {
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
@@ -272,65 +361,164 @@ public class MonsterRepository {
public Completable importBinder(@NonNull BinderExport binder) {
return Completable.fromAction(() -> {
- if (binder.collections == null || binder.collections.isEmpty()) {
- return;
- }
+ // 1. Process Collections & Cards in Binder
+ if (binder.collections != null && !binder.collections.isEmpty()) {
+ List existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
- for (BinderExport.CollectionExport colExport : binder.collections) {
- if (colExport.cards == null || colExport.cards.isEmpty()) {
- continue;
- }
-
- List monstersToSave = new ArrayList<>();
- for (Monster card : colExport.cards) {
- if (card.id == null) {
- card.id = UUID.randomUUID();
+ for (BinderExport.CollectionExport colExport : binder.collections) {
+ if (colExport == null) {
+ continue;
+ }
+
+ String colName = colExport.name != null ? colExport.name.trim() : "";
+ String colDesc = colExport.description != null ? colExport.description.trim() : "";
+
+ UUID colExportId = null;
+ if (colExport.id != null && !colExport.id.trim().isEmpty()) {
+ try {
+ colExportId = UUID.fromString(colExport.id.trim());
+ } catch (Exception ignored) {
+ }
}
- monstersToSave.add(card);
- }
- m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
- String colName = colExport.name != null ? colExport.name.trim() : "";
- if (!colName.isEmpty()) {
- List existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
Collection targetCollection = null;
- for (Collection existing : existingCols) {
- if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
- targetCollection = existing;
- break;
+ if (colExportId != null) {
+ for (Collection existing : existingCols) {
+ if (existing.id.equals(colExportId)) {
+ targetCollection = existing;
+ break;
+ }
+ }
+ }
+ if (targetCollection == null && !colName.isEmpty()) {
+ for (Collection existing : existingCols) {
+ if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
+ targetCollection = existing;
+ break;
+ }
}
}
UUID targetCollectionId;
if (targetCollection != null) {
targetCollectionId = targetCollection.id;
+ if (!colDesc.isEmpty() && (targetCollection.description == null || targetCollection.description.isEmpty())) {
+ targetCollection.description = colDesc;
+ m_db.collectionDAO().save(targetCollection).blockingAwait();
+ }
} else {
Collection newCol = new Collection();
- newCol.id = UUID.randomUUID();
- newCol.name = colName;
+ newCol.id = colExportId != null ? colExportId : UUID.randomUUID();
+ newCol.name = !colName.isEmpty() ? colName : "Imported Collection";
+ newCol.description = colDesc;
m_db.collectionDAO().save(newCol).blockingAwait();
targetCollectionId = newCol.id;
+ existingCols.add(newCol);
}
- List colMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
- .first(new ArrayList<>()).blockingGet();
- Set existingMonsterIds = new HashSet<>();
- for (Monster m : colMonsters) {
- existingMonsterIds.add(m.id);
- }
+ if (colExport.cards != null && !colExport.cards.isEmpty()) {
+ List monstersToSave = new ArrayList<>();
+ Map importedCardCounts = new HashMap<>();
- int ordinal = colMonsters.size();
- for (Monster monster : monstersToSave) {
- if (!existingMonsterIds.contains(monster.id)) {
- m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monster.id, ordinal++)).blockingAwait();
- existingMonsterIds.add(monster.id);
+ for (Monster card : colExport.cards) {
+ if (card != null) {
+ if (card.id == null) {
+ card.id = UUID.randomUUID();
+ }
+ monstersToSave.add(card);
+ importedCardCounts.put(card.id, getCount(importedCardCounts, card.id) + 1);
+ }
+ }
+
+ if (!monstersToSave.isEmpty()) {
+ m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
+
+ List existingColMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
+ .first(new ArrayList<>()).blockingGet();
+ Map existingCardCounts = new HashMap<>();
+ for (Monster m : existingColMonsters) {
+ existingCardCounts.put(m.id, getCount(existingCardCounts, m.id) + 1);
+ }
+
+ int ordinal = existingColMonsters.size();
+ for (Map.Entry entry : importedCardCounts.entrySet()) {
+ UUID monsterId = entry.getKey();
+ int importedCount = entry.getValue();
+ int existingCount = getCount(existingCardCounts, monsterId);
+
+ if (importedCount > existingCount) {
+ int extraCopiesNeeded = importedCount - existingCount;
+ for (int k = 0; k < extraCopiesNeeded; k++) {
+ m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monsterId, ordinal++)).blockingAwait();
+ }
+ }
+ }
}
}
}
}
+
+ // 2. Process Dashboard entries in Binder if present
+ if (binder.dashboard != null && !binder.dashboard.isEmpty()) {
+ List dashboardMonstersToSave = new ArrayList<>();
+ Map importedDashboardCounts = new HashMap<>();
+
+ for (Monster card : binder.dashboard) {
+ if (card != null) {
+ if (card.id == null) {
+ card.id = UUID.randomUUID();
+ }
+ dashboardMonstersToSave.add(card);
+ importedDashboardCounts.put(card.id, getCount(importedDashboardCounts, card.id) + 1);
+ }
+ }
+
+ if (!dashboardMonstersToSave.isEmpty()) {
+ m_db.monsterDAO().save(dashboardMonstersToSave.toArray(new Monster[0])).blockingAwait();
+
+ List existingDashboard = m_db.dashboardDAO().getDashboardMonstersWithMonster()
+ .first(new ArrayList<>()).blockingGet();
+ Map existingDashboardCounts = new HashMap<>();
+ int currentMaxOrdinal = 0;
+ for (DashboardMonsterWithMonster entry : existingDashboard) {
+ if (entry.monster != null) {
+ existingDashboardCounts.put(entry.monster.id, getCount(existingDashboardCounts, entry.monster.id) + 1);
+ }
+ if (entry.dashboardEntry != null && entry.dashboardEntry.ordinal > currentMaxOrdinal) {
+ currentMaxOrdinal = entry.dashboardEntry.ordinal;
+ }
+ }
+
+ int ordinal = currentMaxOrdinal + 1;
+ List extraEntriesToAdd = new ArrayList<>();
+
+ for (Map.Entry entry : importedDashboardCounts.entrySet()) {
+ UUID monsterId = entry.getKey();
+ int importedCount = entry.getValue();
+ int existingCount = getCount(existingDashboardCounts, monsterId);
+
+ if (importedCount > existingCount) {
+ int extraNeeded = importedCount - existingCount;
+ for (int k = 0; k < extraNeeded; k++) {
+ extraEntriesToAdd.add(new DashboardMonster(monsterId, ordinal++));
+ }
+ }
+ }
+
+ if (!extraEntriesToAdd.isEmpty()) {
+ m_db.dashboardDAO().addMonsterToDashboard(extraEntriesToAdd.toArray(new DashboardMonster[0])).blockingAwait();
+ }
+ }
+ }
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
+ private static int getCount(Map map, UUID key) {
+ if (map == null || key == null) return 0;
+ Integer val = map.get(key);
+ return val != null ? val : 0;
+ }
+
private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) {
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
index 5a0b24e..d43b91f 100644
--- 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
@@ -21,11 +21,23 @@ public enum AbilityScore {
}
public static AbilityScore valueOfString(String string) {
+ if (string == null) {
+ return AbilityScore.STRENGTH;
+ }
+ String lower = string.trim().toLowerCase(java.util.Locale.ROOT);
for (AbilityScore abilityScore : values()) {
- if (abilityScore.stringValue.equals(string)) {
+ if (abilityScore.stringValue.equalsIgnoreCase(lower)
+ || abilityScore.shortDisplayName.equalsIgnoreCase(lower)
+ || abilityScore.name().equalsIgnoreCase(lower)) {
return abilityScore;
}
}
+ if (lower.startsWith("str")) return STRENGTH;
+ if (lower.startsWith("dex")) return DEXTERITY;
+ if (lower.startsWith("con")) return CONSTITUTION;
+ if (lower.startsWith("int")) return INTELLIGENCE;
+ if (lower.startsWith("wis")) return WISDOM;
+ if (lower.startsWith("cha")) return CHARISMA;
return AbilityScore.STRENGTH;
}
}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java
index b988333..28feb97 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java
@@ -1,34 +1,94 @@
package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.majinnaibu.monstercards.models.BinderExport;
+import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster;
import java.util.Collections;
import java.util.List;
+import java.util.UUID;
public class BinderExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull
public String exportBinder(String collectionName, List monsters) {
+ return exportBinder(new Collection(collectionName != null ? collectionName : "", ""), monsters, null);
+ }
+
+ @NonNull
+ public String exportBinder(@Nullable Collection collection, List monsters) {
+ return exportBinder(collection, monsters, null);
+ }
+
+ @NonNull
+ public String exportBinder(@Nullable Collection collection, @Nullable List monsters, @Nullable List dashboardMonsters) {
BinderExport export = new BinderExport();
+ export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
export.schemaVersion = 1;
if (monsters != null) {
for (Monster monster : monsters) {
+ monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1;
}
}
- BinderExport.CollectionExport col = new BinderExport.CollectionExport(
- collectionName != null ? collectionName : "",
- monsters
- );
- export.collections = Collections.singletonList(col);
+ if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
+ for (Monster monster : dashboardMonsters) {
+ monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
+ monster.schemaVersion = 1;
+ }
+ export.dashboard = dashboardMonsters;
+ }
+
+ if (collection != null) {
+ String colId = collection.id.toString();
+ String colName = collection.name;
+ String colDesc = collection.description;
+
+ BinderExport.CollectionExport col = new BinderExport.CollectionExport(
+ colId,
+ colName,
+ colDesc,
+ monsters
+ );
+ export.collections = Collections.singletonList(col);
+ }
+ return GSON.toJson(export);
+ }
+
+ @NonNull
+ public String exportFullBackup(@Nullable List collections, @Nullable List dashboardMonsters) {
+ BinderExport export = new BinderExport();
+ export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
+ export.schemaVersion = 1;
+
+ if (collections != null) {
+ for (BinderExport.CollectionExport col : collections) {
+ if (col.cards != null) {
+ for (Monster m : col.cards) {
+ m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
+ m.schemaVersion = 1;
+ }
+ }
+ }
+ export.collections = collections;
+ }
+
+ if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
+ for (Monster m : dashboardMonsters) {
+ m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
+ m.schemaVersion = 1;
+ }
+ export.dashboard = dashboardMonsters;
+ }
+
return GSON.toJson(export);
}
}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java
index 96bd982..660a5db 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java
@@ -11,6 +11,7 @@ public class MonsterCardExporter {
@NonNull
public String exportCard(@NonNull Monster monster) {
+ monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1;
return GSON.toJson(monster);
}
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
index 1d3c080..ae6fd02 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java
@@ -1,39 +1,85 @@
package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.EntityImporter;
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
import com.majinnaibu.monstercards.importers.Open5eImporter;
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
import com.majinnaibu.monstercards.models.Monster;
+import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
public class MonsterImportHelper {
private static final List> IMPORTERS = new ArrayList<>();
static {
+ IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new MonsterJsonImporter());
IMPORTERS.add(new Open5eImporter());
- IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new DnDBeyondImporter());
}
@NonNull
public static Monster fromJSON(String json) {
- if (json != null) {
- for (EntityImporter importer : IMPORTERS) {
- if (importer.canImport(json)) {
- try {
- return importer.parse(json);
- } catch (Exception e) {
- throw new IllegalArgumentException("Failed to parse monster JSON", e);
+ return fromJSON(json, null);
+ }
+
+ @NonNull
+ public static Monster fromJSON(String json, @Nullable String fileName) {
+ if (json == null || json.trim().isEmpty()) {
+ throw new IllegalArgumentException("JSON payload is null or empty");
+ }
+
+ // 1. Check for explicit $schema reference matching monster-card or tetracube schema
+ try {
+ JsonElement el = JsonParser.parseString(json);
+ if (el.isJsonObject()) {
+ JsonObject obj = el.getAsJsonObject();
+ if (obj.has("$schema")) {
+ String schemaVal = obj.get("$schema").getAsString();
+ if (schemaVal.contains("monster-card.schema.json")) {
+ return new MonsterJsonImporter().parse(json);
+ } else if (schemaVal.contains("tetracube-monster.schema.json")) {
+ return new TetraCubeMonsterImporter().parse(json);
}
}
}
+ } catch (Exception e) {
+ Logger.logError("Error checking $schema reference tag", e);
+ }
+
+ // 2. If filename ends in .monster, try TetraCube importer first
+ String lowerFileName = fileName != null ? fileName.toLowerCase(Locale.ROOT) : "";
+ boolean isMonsterFile = lowerFileName.endsWith(".monster") || lowerFileName.endsWith(".monster.txt");
+ if (isMonsterFile) {
+ TetraCubeMonsterImporter tetraImporter = new TetraCubeMonsterImporter();
+ if (tetraImporter.canImport(json)) {
+ try {
+ return tetraImporter.parse(json);
+ } catch (Exception e) {
+ Logger.logError("TetraCube importer failed for .monster file: " + fileName, e);
+ }
+ }
+ }
+
+ // 3. Fallback to current importer detection chain
+ for (EntityImporter importer : IMPORTERS) {
+ if (importer.canImport(json)) {
+ try {
+ return importer.parse(json);
+ } catch (Exception e) {
+ Logger.logError("Importer " + importer.getClass().getSimpleName() + " failed", e);
+ }
+ }
}
throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload.");
}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java
index d2f97df..3d49526 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java
@@ -18,6 +18,12 @@ public class BinderImporter implements EntityImporter {
JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
+ if (obj.has("$schema")) {
+ String schemaVal = obj.get("$schema").getAsString();
+ if (schemaVal.contains("binder.schema.json")) {
+ return true;
+ }
+ }
return obj.has("collections") && obj.get("collections").isJsonArray();
}
} catch (Exception ignored) {
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java
index adc00a9..beb7284 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java
@@ -18,7 +18,13 @@ public class MonsterJsonImporter implements EntityImporter {
JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
- return obj.has("name") && (obj.has("strengthScore") || obj.has("hitDice") || obj.has("size"));
+ if (obj.has("$schema")) {
+ String schemaVal = obj.get("$schema").getAsString();
+ if (schemaVal.contains("monster-card.schema.json")) {
+ return true;
+ }
+ }
+ return obj.has("name") && (obj.has("strengthScore") || obj.has("strengthSavingThrowProficiency"));
}
} catch (Exception ignored) {
}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java
index 967bee2..63dc39b 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java
@@ -36,6 +36,12 @@ public class TetraCubeMonsterImporter implements EntityImporter {
return false;
}
JsonObject rootDict = element.getAsJsonObject();
+ if (rootDict.has("$schema")) {
+ String schemaVal = rootDict.get("$schema").getAsString();
+ if (schemaVal.contains("tetracube-monster.schema.json")) {
+ return true;
+ }
+ }
return rootDict.has("armorName") || rootDict.has("strPoints")
|| rootDict.has("isLegendary") || rootDict.has("isMythic")
|| rootDict.has("legendariesDescription");
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java
index f9fcc00..df9c36e 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java
@@ -4,27 +4,46 @@ import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
public class BinderExport {
+ @SerializedName("$schema")
+ public String schema = "https://majinnaibu.com/schemas/binder.schema.json";
+
@SerializedName("schemaVersion")
public int schemaVersion = 1;
@SerializedName("collections")
public List collections = new ArrayList<>();
+ @SerializedName("dashboard")
+ public List dashboard = new ArrayList<>();
+
public static class CollectionExport {
+ @SerializedName("id")
+ public String id;
+
@SerializedName("name")
public String name;
+ @SerializedName("description")
+ public String description;
+
@SerializedName("cards")
public List cards = new ArrayList<>();
public CollectionExport() {
}
- public CollectionExport(String name, List cards) {
+ public CollectionExport(String id, String name, String description, List cards) {
+ this.id = id != null ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
+ this.description = description != null ? description : "";
this.cards = cards != null ? cards : new ArrayList<>();
}
+
+ public CollectionExport(String name, List cards) {
+ this(UUID.randomUUID().toString(), name, "", cards);
+ }
}
}
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
index 75c29bf..326bb54 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
@@ -9,6 +9,7 @@ import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
+import com.google.gson.annotations.SerializedName;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType;
@@ -30,11 +31,16 @@ import java.util.UUID;
@SuppressWarnings("unused")
public class Monster {
+ @SerializedName("$schema")
+ @Ignore
+ public String schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
+
@Ignore
public int schemaVersion = 1;
@PrimaryKey
@NonNull
+ @SerializedName("id")
public UUID id;
@NonNull
@@ -438,7 +444,10 @@ public class Monster {
}
}
- public int getAbilityModifier(@NonNull AbilityScore abilityScore) {
+ public int getAbilityModifier(@Nullable AbilityScore abilityScore) {
+ if (abilityScore == null) {
+ return 0;
+ }
switch (abilityScore) {
case STRENGTH:
return getStrengthModifier();
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
index 9730cc5..5e202f4 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java
@@ -34,13 +34,25 @@ public class Skill implements Comparator, Comparable {
this.proficiencyType = proficiencyType;
}
+ public AbilityScore getAbilityScore() {
+ if (abilityScore != null) {
+ return abilityScore;
+ }
+ if (name != null) {
+ return AbilityScore.valueOfString(name);
+ }
+ return AbilityScore.STRENGTH;
+ }
+
public int getSkillBonus(Monster monster) {
- int modifier = monster.getAbilityModifier(abilityScore);
- switch (proficiencyType) {
+ AbilityScore score = getAbilityScore();
+ int modifier = monster != null ? monster.getAbilityModifier(score) : 0;
+ ProficiencyType prof = proficiencyType != null ? proficiencyType : ProficiencyType.PROFICIENT;
+ switch (prof) {
case PROFICIENT:
- return modifier + monster.getProficiencyBonus();
+ return modifier + (monster != null ? monster.getProficiencyBonus() : 0);
case EXPERTISE:
- return modifier + monster.getProficiencyBonus() * 2;
+ return modifier + (monster != null ? monster.getProficiencyBonus() * 2 : 0);
case NONE:
default:
return modifier;
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java
index c8cc731..d73e0d6 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java
@@ -221,10 +221,32 @@ public class CollectionDetailFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_export_collection) {
exportCollection();
return true;
+ } else if (item.getItemId() == R.id.menu_action_remove_all_monsters_from_collection) {
+ showRemoveAllMonstersConfirmationDialog();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void showRemoveAllMonstersConfirmationDialog() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_remove_all_from_collection_title)
+ .setMessage(R.string.dialog_remove_all_from_collection_message)
+ .setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
+ mDisposables.add(getMonsterRepository().removeAllMonstersFromCollection(mCollectionId)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ View view = getView();
+ if (view != null) {
+ SnackbarHelper.showLong(view, R.string.snackbar_collection_cleared);
+ }
+ }, throwable -> Logger.logError("Failed to remove all monsters from collection", throwable)));
+ })
+ .setNegativeButton(R.string.dialog_cancel, null)
+ .show();
+ }
+
private void exportCollection() {
Collection collection = mViewModel.getCollection().getValue();
List monsters = mViewModel.getMonsters().getValue();
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
index 4cfae01..3262ae9 100644
--- 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
@@ -4,6 +4,9 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
@@ -43,6 +46,7 @@ public class CollectionsFragment extends MCFragment {
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
View root = inflater.inflate(R.layout.fragment_collections, container, false);
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
@@ -190,6 +194,51 @@ public class CollectionsFragment extends MCFragment {
Navigation.findNavController(requireView()).navigate(action);
}
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ inflater.inflate(R.menu.collections_menu, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.menu_action_export_collections) {
+ exportCollections();
+ return true;
+ } else if (item.getItemId() == R.id.menu_action_remove_all_collections) {
+ showRemoveAllCollectionsConfirmationDialog();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void exportCollections() {
+ mDisposables.add(getMonsterRepository().exportCollections()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(json -> exportToFile("collections.binder", json),
+ throwable -> Logger.logError("Failed to export collections", throwable)));
+ }
+
+ private void showRemoveAllCollectionsConfirmationDialog() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_remove_all_collections_title)
+ .setMessage(R.string.dialog_remove_all_collections_message)
+ .setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
+ mDisposables.add(getMonsterRepository().removeAllCollections()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ View view = getView();
+ if (view != null) {
+ SnackbarHelper.showLong(view, R.string.snackbar_all_collections_removed);
+ }
+ }, throwable -> Logger.logError("Failed to remove all collections", throwable)));
+ })
+ .setNegativeButton(R.string.dialog_cancel, null)
+ .show();
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
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
index 153ed3f..64b01c2 100644
--- 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
@@ -24,6 +24,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Collection;
+import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
@@ -265,7 +266,7 @@ public class DashboardFragment extends MCFragment {
private void showAddCollectionPicker() {
MonsterRepository repository = getMonsterRepository();
- mDisposables.add(repository.getCollections()
+ mDisposables.add(repository.getCollectionsWithCount()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@@ -279,12 +280,16 @@ public class DashboardFragment extends MCFragment {
}
String[] names = new String[collections.size()];
for (int i = 0; i < collections.size(); i++) {
- names[i] = collections.get(i).name;
+ CollectionWithCount item = collections.get(i);
+ int count = item.monsterCount;
+ String countText = count == 1 ? "1 monster" : count + " monsters";
+ String colName = item.collection != null && item.collection.name != null ? item.collection.name : "";
+ names[i] = colName + " (" + countText + ")";
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_add_collection_option)
.setItems(names, (dialog, which) -> {
- Collection selected = collections.get(which);
+ Collection selected = collections.get(which).collection;
addCollectionToDashboard(selected);
})
.setNegativeButton(R.string.dialog_cancel, null)
@@ -365,9 +370,6 @@ public class DashboardFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_add_collection_option) {
showAddCollectionPicker();
return true;
- } else if (item.getItemId() == R.id.menu_action_remove_single_monster) {
- showRemoveMonsterPicker();
- return true;
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
clearDashboard();
return true;
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java
index 6c6166d..a10b683 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java
@@ -85,10 +85,35 @@ public class LibraryFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_export_library) {
exportLibrary();
return true;
+ } else if (item.getItemId() == R.id.menu_action_export_everything) {
+ exportEverything();
+ return true;
+ } else if (item.getItemId() == R.id.menu_action_clear_all_data) {
+ showClearAllDataConfirmationDialog();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void showClearAllDataConfirmationDialog() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_clear_all_data_title)
+ .setMessage(R.string.dialog_clear_all_data_message)
+ .setPositiveButton(R.string.action_clear, (dialog, which) -> {
+ mDisposables.add(getMonsterRepository().clearAllData()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ View view = getView();
+ if (view != null) {
+ SnackbarHelper.showLong(view, R.string.snackbar_all_data_cleared);
+ }
+ }, throwable -> Logger.logError("Failed to clear all data", throwable)));
+ })
+ .setNegativeButton(R.string.dialog_cancel, null)
+ .show();
+ }
+
private void exportLibrary() {
getMonsterRepository().getMonsters()
.firstOrError()
@@ -101,6 +126,16 @@ public class LibraryFragment extends MCFragment {
}, Logger::logError);
}
+ private void exportEverything() {
+ mDisposables.add(getMonsterRepository().exportEverything()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(json -> {
+ String fileName = getString(R.string.default_filename_export_everything) + ".binder";
+ exportToFile(fileName, json);
+ }, throwable -> Logger.logError("Failed to export everything", throwable)));
+ }
+
private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
Context context = requireContext();
MonsterRepository repository = this.getMonsterRepository();
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java
index bd032cf..b65b202 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java
@@ -40,6 +40,8 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -71,17 +73,22 @@ public class MCFragment extends Fragment {
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
- Uri uri = result.getData().getData();
- if (uri != null && getActivity() instanceof MainActivity) {
- String content = readContentsFromUri(uri);
- if (content != null && !content.trim().isEmpty()) {
- ((MainActivity) getActivity()).importMonsterFromInputAndNavigate(content);
- } else {
- View view = getView();
- if (view != null) {
- SnackbarHelper.showLong(view, R.string.failed_to_import_url);
+ Intent data = result.getData();
+ List uris = new ArrayList<>();
+ if (data.getClipData() != null) {
+ ClipData clipData = data.getClipData();
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ Uri uri = clipData.getItemAt(i).getUri();
+ if (uri != null) {
+ uris.add(uri);
}
}
+ } else if (data.getData() != null) {
+ uris.add(data.getData());
+ }
+
+ if (!uris.isEmpty() && getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).importMultipleFilesFromUris(uris);
}
}
});
@@ -91,6 +98,7 @@ public class MCFragment extends Fragment {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
try {
mOpenDocumentLauncher.launch(intent);
} catch (Exception e) {
diff --git a/Android/app/src/main/res/menu/collection_detail_menu.xml b/Android/app/src/main/res/menu/collection_detail_menu.xml
index 02cd759..48376a0 100644
--- a/Android/app/src/main/res/menu/collection_detail_menu.xml
+++ b/Android/app/src/main/res/menu/collection_detail_menu.xml
@@ -11,4 +11,9 @@
android:id="@+id/menu_action_export_collection"
android:title="@string/action_export_collection"
app:showAsAction="never" />
+
+
diff --git a/Android/app/src/main/res/menu/collections_menu.xml b/Android/app/src/main/res/menu/collections_menu.xml
new file mode 100644
index 0000000..a312811
--- /dev/null
+++ b/Android/app/src/main/res/menu/collections_menu.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/Android/app/src/main/res/menu/dashboard_menu.xml b/Android/app/src/main/res/menu/dashboard_menu.xml
index 8d6eacf..23180bc 100644
--- a/Android/app/src/main/res/menu/dashboard_menu.xml
+++ b/Android/app/src/main/res/menu/dashboard_menu.xml
@@ -12,11 +12,6 @@
android:title="@string/action_add_collection_option"
app:showAsAction="never" />
-
-
+
+
+
+
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
index 3c343c8..04e2523 100644
--- a/Android/app/src/main/res/values/strings.xml
+++ b/Android/app/src/main/res/values/strings.xml
@@ -11,7 +11,7 @@
Add Skill
Add Trait
Edit
- Import Monster
+ Import File
MonsterCards
CHA
CON
@@ -133,7 +133,7 @@
Strings
Trait
Traits
- Import Monster
+ Import File
Library
Monster Details
%1$s Details
@@ -156,25 +156,25 @@
Add Collection to Dashboard
Add Monster
Add Collection
- Clear Dashboard
+ Clear
Add to Dashboard
Added %1$s to Dashboard
Added collection %1$s to Dashboard
Dashboard cleared
No monsters available in library. Create one first!
- Import URL...
- Import Entity or Character
+ Import URL
+ Import Character
Enter or paste a D&D Beyond URL, Character ID, or Open5e JSON payload:
e.g. D&D Beyond URL or Open5e JSON
Import
- Importing entity...
- Failed to import entity. Please check the URL, ID, or JSON format.
+ Importing…
+ Failed to import URL. Please check the URL, ID, or JSON format.
Share / Export Monster (.card)
Failed to share monster
Export Card
Export Collection
Export Library
- Export Dashboard
+ Export
Exported to %1$s
Failed to export file
Imported binder successfully
@@ -189,7 +189,7 @@
Create new monster cards or import them from D&D Beyond or Open5e.
Monster Cards
- Remove Monster...
+ Remove Monster
Remove
Undo
View Details
@@ -200,10 +200,29 @@
Remove %1$s from your dashboard?
Create Monster
- Import Monster from Url
- Import Monster from File
- Import Collection
+ Import URL
+ Import File
+ Import File
Library Actions
Collection Actions
Dashboard Actions
+
+ Clear
+ Clear All Data
+ Clear All Data?
+ This will permanently delete all monsters, collections, and clear your dashboard. Are you sure?
+ All data cleared
+
+ Export All Data
+ monster_cards_backup
+
+ Export
+ Remove All
+ Remove All Collections?
+ Are you sure you want to remove all collections? Monsters in your library will not be deleted.
+ All collections removed
+
+ Remove All Monsters?
+ Are you sure you want to remove all monsters from this collection?
+ Removed all monsters from collection
diff --git a/monsters/Unnamed Monster.card b/monsters/Unnamed Monster.card
new file mode 100644
index 0000000..ae2c145
--- /dev/null
+++ b/monsters/Unnamed Monster.card
@@ -0,0 +1,84 @@
+{
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+}
\ No newline at end of file
diff --git a/monsters/baddies (1).binder b/monsters/baddies (1).binder
new file mode 100644
index 0000000..e4f724a
--- /dev/null
+++ b/monsters/baddies (1).binder
@@ -0,0 +1,514 @@
+{
+ "collections": [
+ {
+ "cards": [
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ }
+ ],
+ "name": "baddies"
+ }
+ ],
+ "schemaVersion": 1
+}
\ No newline at end of file
diff --git a/monsters/baddies.binder b/monsters/baddies.binder
new file mode 100644
index 0000000..e4f724a
--- /dev/null
+++ b/monsters/baddies.binder
@@ -0,0 +1,514 @@
+{
+ "collections": [
+ {
+ "cards": [
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ },
+ {
+ "abilities": [],
+ "actions": [],
+ "age": "",
+ "alignment": "",
+ "alliesAndOrganizations": "",
+ "appearance": "",
+ "armorType": "NONE",
+ "background": "",
+ "backstory": "",
+ "bonds": "",
+ "bonusActions": [],
+ "burrowSpeed": 0,
+ "canHover": false,
+ "challengeRating": "ONE",
+ "charismaSavingThrowAdvantage": "NONE",
+ "charismaSavingThrowProficiency": "NONE",
+ "charismaScore": 10,
+ "climbSpeed": 0,
+ "conditionImmunities": [],
+ "constitutionSavingThrowAdvantage": "NONE",
+ "constitutionSavingThrowProficiency": "NONE",
+ "constitutionScore": 10,
+ "customChallengeRatingDescription": "",
+ "customHPDescription": "",
+ "customProficiencyBonus": 0,
+ "customSpeedDescription": "",
+ "damageImmunities": [],
+ "damageResistances": [],
+ "damageVulnerabilities": [],
+ "dexteritySavingThrowAdvantage": "NONE",
+ "dexteritySavingThrowProficiency": "NONE",
+ "dexterityScore": 10,
+ "eyes": "",
+ "flaws": "",
+ "flySpeed": 0,
+ "hair": "",
+ "hasCustomHP": false,
+ "hasCustomSpeed": false,
+ "height": "",
+ "hitDice": 1,
+ "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b",
+ "ideals": "",
+ "intelligenceSavingThrowAdvantage": "NONE",
+ "intelligenceSavingThrowProficiency": "NONE",
+ "intelligenceScore": 10,
+ "lairActions": [],
+ "lairActionsDescription": "",
+ "lairActionsEndNote": "",
+ "languages": [],
+ "legendaryActions": [],
+ "legendaryActionsDescription": "",
+ "mythicActions": [],
+ "mythicActionsDescription": "",
+ "name": "Unnamed Monster",
+ "naturalArmorBonus": 0,
+ "otherArmorDescription": "",
+ "personalityTraits": "",
+ "playerName": "",
+ "reactions": [],
+ "regionalActions": [],
+ "regionalActionsDescription": "",
+ "regionalActionsEndNote": "",
+ "schemaVersion": 1,
+ "senses": [],
+ "shieldBonus": 0,
+ "size": "",
+ "skills": [],
+ "skin": "",
+ "sourceUrl": "",
+ "strengthSavingThrowAdvantage": "NONE",
+ "strengthSavingThrowProficiency": "NONE",
+ "strengthScore": 10,
+ "subtype": "",
+ "swimSpeed": 0,
+ "telepathyRange": 0,
+ "type": "",
+ "understandsButDescription": "",
+ "walkSpeed": 0,
+ "weight": "",
+ "wisdomSavingThrowAdvantage": "NONE",
+ "wisdomSavingThrowProficiency": "NONE",
+ "wisdomScore": 10
+ }
+ ],
+ "name": "baddies"
+ }
+ ],
+ "schemaVersion": 1
+}
\ No newline at end of file
diff --git a/schemas/binder.schema.json b/schemas/binder.schema.json
new file mode 100644
index 0000000..02ef68c
--- /dev/null
+++ b/schemas/binder.schema.json
@@ -0,0 +1,42 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://majinnaibu.com/schemas/binder.schema.json",
+ "title": "BinderExport",
+ "description": "JSON Schema for MonsterCards .binder collection files",
+ "type": "object",
+ "required": [
+ "collections",
+ "schemaVersion"
+ ],
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "schemaVersion": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "collections": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "cards"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "cards": {
+ "type": "array"
+ }
+ }
+ }
+ },
+ "dashboard": {
+ "type": "array"
+ }
+ }
+}
diff --git a/schemas/monster-card.schema.json b/schemas/monster-card.schema.json
new file mode 100644
index 0000000..af01e44
--- /dev/null
+++ b/schemas/monster-card.schema.json
@@ -0,0 +1,87 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://majinnaibu.com/schemas/monster-card.schema.json",
+ "title": "MonsterCard",
+ "description": "JSON Schema for MonsterCards .card files",
+ "type": "object",
+ "required": [
+ "name",
+ "schemaVersion"
+ ],
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "schemaVersion": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "size": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "subtype": {
+ "type": "string"
+ },
+ "alignment": {
+ "type": "string"
+ },
+ "strengthScore": {
+ "type": "integer"
+ },
+ "dexterityScore": {
+ "type": "integer"
+ },
+ "constitutionScore": {
+ "type": "integer"
+ },
+ "intelligenceScore": {
+ "type": "integer"
+ },
+ "wisdomScore": {
+ "type": "integer"
+ },
+ "charismaScore": {
+ "type": "integer"
+ },
+ "hitDice": {
+ "type": "integer"
+ },
+ "walkSpeed": {
+ "type": "integer"
+ },
+ "burrowSpeed": {
+ "type": "integer"
+ },
+ "climbSpeed": {
+ "type": "integer"
+ },
+ "flySpeed": {
+ "type": "integer"
+ },
+ "swimSpeed": {
+ "type": "integer"
+ },
+ "abilities": {
+ "type": "array"
+ },
+ "actions": {
+ "type": "array"
+ },
+ "reactions": {
+ "type": "array"
+ },
+ "legendaryActions": {
+ "type": "array"
+ }
+ }
+}
diff --git a/schemas/tetracube-monster.schema.json b/schemas/tetracube-monster.schema.json
new file mode 100644
index 0000000..cfb855b
--- /dev/null
+++ b/schemas/tetracube-monster.schema.json
@@ -0,0 +1,257 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://majinnaibu.com/schemas/tetracube-monster.schema.json",
+ "title": "TetraCubeMonster",
+ "description": "JSON Schema for TetraCube .monster files based on all_the_fields.monster",
+ "type": "object",
+ "required": [
+ "name",
+ "size",
+ "type"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "size": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "tag": {
+ "type": "string"
+ },
+ "race": {
+ "type": "string"
+ },
+ "class": {
+ "type": "string"
+ },
+ "alignment": {
+ "type": "string"
+ },
+ "armorName": {
+ "type": "string"
+ },
+ "strPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "dexPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "conPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "intPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "wisPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "chaPoints": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "hitDice": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "speed": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "burrowSpeed": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "climbSpeed": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "flySpeed": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "swimSpeed": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "cr": {
+ "type": "string"
+ },
+ "customCr": {
+ "type": "string"
+ },
+ "customProf": {
+ "type": "integer"
+ },
+ "shieldBonus": {
+ "type": "integer"
+ },
+ "natArmorBonus": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "otherArmorDesc": {
+ "type": "string"
+ },
+ "hpText": {
+ "type": "string"
+ },
+ "speedDesc": {
+ "type": "string"
+ },
+ "customHP": {
+ "type": "boolean"
+ },
+ "customSpeed": {
+ "type": "boolean"
+ },
+ "hover": {
+ "type": "boolean"
+ },
+ "blind": {
+ "type": "boolean"
+ },
+ "blindsight": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "darkvision": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "tremorsense": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "truesight": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "telepathy": {
+ "type": "integer"
+ },
+ "understandsBut": {
+ "type": "string"
+ },
+ "shortName": {
+ "type": "string"
+ },
+ "pluralName": {
+ "type": "string"
+ },
+ "isLegendary": {
+ "type": "boolean"
+ },
+ "legendariesDescription": {
+ "type": "string"
+ },
+ "isMythic": {
+ "type": "boolean"
+ },
+ "mythicDescription": {
+ "type": "string"
+ },
+ "isLair": {
+ "type": "boolean"
+ },
+ "lairDescription": {
+ "type": "string"
+ },
+ "lairDescriptionEnd": {
+ "type": "string"
+ },
+ "isRegional": {
+ "type": "boolean"
+ },
+ "regionalDescription": {
+ "type": "string"
+ },
+ "regionalDescriptionEnd": {
+ "type": "string"
+ },
+ "abilities": {
+ "type": "array"
+ },
+ "actions": {
+ "type": "array"
+ },
+ "bonusActions": {
+ "type": "array"
+ },
+ "reactions": {
+ "type": "array"
+ },
+ "legendaries": {
+ "type": "array"
+ },
+ "mythics": {
+ "type": "array"
+ },
+ "lairs": {
+ "type": "array"
+ },
+ "regionals": {
+ "type": "array"
+ },
+ "skills": {
+ "type": "array"
+ },
+ "sthrows": {
+ "type": "array"
+ },
+ "languages": {
+ "type": "array"
+ },
+ "conditions": {
+ "type": "array"
+ },
+ "damagetypes": {
+ "type": "array"
+ },
+ "specialdamage": {
+ "type": "array"
+ }
+ }
+}