Cleans up import and export options.

This commit is contained in:
2026-09-20 19:10:43 -07:00
parent 77176a6d4f
commit a3998d6c1d
31 changed files with 2280 additions and 110 deletions

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -28,6 +28,8 @@ import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.importers.BinderImporter; import com.majinnaibu.monstercards.importers.BinderImporter;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter; import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.init.AppCenterInitializer; 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.Logger;
import com.majinnaibu.monstercards.utils.ToastHelper; import com.majinnaibu.monstercards.utils.ToastHelper;
@@ -35,6 +37,8 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
@@ -131,13 +135,34 @@ public class MainActivity extends AppCompatActivity {
} }
} }
Uri intentUri = getUriFromIntent(intent);
String fileName = intentUri != null ? getFileNameFromUri(intentUri) : null;
String json = readMonsterJSONFromIntent(intent); String json = readMonsterJSONFromIntent(intent);
if (!StringHelper.isNullOrEmpty(json)) { 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) { public void importMonsterFromInputAndNavigate(@NonNull String input) {
importMonsterFromInputAndNavigate(input, null);
}
public void importMonsterFromInputAndNavigate(@NonNull String input, @Nullable String fileName) {
try {
BinderImporter binderImporter = new BinderImporter(); BinderImporter binderImporter = new BinderImporter();
if (binderImporter.canImport(input)) { if (binderImporter.canImport(input)) {
ToastHelper.showShort(this, R.string.toast_importing_url); ToastHelper.showShort(this, R.string.toast_importing_url);
@@ -151,14 +176,14 @@ public class MainActivity extends AppCompatActivity {
NavController navController = navHostFragment.getNavController(); NavController navController = navHostFragment.getNavController();
navController.navigate(R.id.navigation_library); navController.navigate(R.id.navigation_library);
}, throwable -> { }, throwable -> {
Logger.logError("Failed to import binder from input", throwable); Logger.logError("Failed to import binder from input: " + fileName, throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url); ToastHelper.showLong(this, R.string.failed_to_import_url);
}); });
return; return;
} }
ToastHelper.showShort(this, R.string.toast_importing_url); ToastHelper.showShort(this, R.string.toast_importing_url);
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input)) Single.fromCallable(() -> MonsterImportHelper.fromJSON(input, fileName))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(monster -> { .subscribe(monster -> {
@@ -168,9 +193,103 @@ public class MainActivity extends AppCompatActivity {
NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson); NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
navController.navigate(navAction); navController.navigate(navAction);
}, throwable -> { }, throwable -> {
Logger.logError("Failed to import monster from input", throwable); Logger.logError("Failed to import monster from input: " + fileName, throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url); 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<Uri> 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(() -> {
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 @Nullable

View File

@@ -47,9 +47,18 @@ public interface CollectionDAO {
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId") @Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
Completable removeMonsterFromCollection(String collectionId, String 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") @Query("DELETE FROM collection_monsters WHERE id = :id")
Completable removeCollectionMonsterById(long id); Completable removeCollectionMonsterById(long id);
@Query("DELETE FROM collection_monsters")
Completable deleteAllCollectionMonsters();
@Query("DELETE FROM collections")
Completable deleteAllCollections();
@Update @Update
Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters); Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters);
} }

View File

@@ -36,4 +36,7 @@ public interface MonsterDAO {
@Delete @Delete
Completable delete(Monster monster); Completable delete(Monster monster);
@Query("DELETE FROM monsters")
Completable deleteAllMonsters();
} }

View File

@@ -9,18 +9,22 @@ import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount; import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.DashboardMonster; import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem; import com.majinnaibu.monstercards.models.SearchResultItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
@@ -264,6 +268,91 @@ public class MonsterRepository {
return result; 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<String> exportCollections() {
return m_db.collectionDAO().getAll().first(new ArrayList<>())
.map(collections -> {
List<BinderExport.CollectionExport> colExports = new ArrayList<>();
for (Collection col : collections) {
List<Monster> 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<String> 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<BinderExport.CollectionExport> colExports = new ArrayList<>();
Set<UUID> monstersInCollections = new HashSet<>();
for (Collection col : collections) {
List<Monster> 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<Monster> 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<DashboardMonster> items) { public Completable updateDashboardMonsters(List<DashboardMonster> items) {
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items); Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
@@ -272,65 +361,164 @@ public class MonsterRepository {
public Completable importBinder(@NonNull BinderExport binder) { public Completable importBinder(@NonNull BinderExport binder) {
return Completable.fromAction(() -> { return Completable.fromAction(() -> {
if (binder.collections == null || binder.collections.isEmpty()) { // 1. Process Collections & Cards in Binder
return; if (binder.collections != null && !binder.collections.isEmpty()) {
} List<Collection> existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
for (BinderExport.CollectionExport colExport : binder.collections) { for (BinderExport.CollectionExport colExport : binder.collections) {
if (colExport.cards == null || colExport.cards.isEmpty()) { if (colExport == null) {
continue; continue;
} }
List<Monster> monstersToSave = new ArrayList<>();
for (Monster card : colExport.cards) {
if (card.id == null) {
card.id = UUID.randomUUID();
}
monstersToSave.add(card);
}
m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
String colName = colExport.name != null ? colExport.name.trim() : ""; String colName = colExport.name != null ? colExport.name.trim() : "";
if (!colName.isEmpty()) { String colDesc = colExport.description != null ? colExport.description.trim() : "";
List<Collection> existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
UUID colExportId = null;
if (colExport.id != null && !colExport.id.trim().isEmpty()) {
try {
colExportId = UUID.fromString(colExport.id.trim());
} catch (Exception ignored) {
}
}
Collection targetCollection = null; Collection targetCollection = null;
if (colExportId != null) {
for (Collection existing : existingCols) {
if (existing.id.equals(colExportId)) {
targetCollection = existing;
break;
}
}
}
if (targetCollection == null && !colName.isEmpty()) {
for (Collection existing : existingCols) { for (Collection existing : existingCols) {
if (existing.name != null && existing.name.equalsIgnoreCase(colName)) { if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
targetCollection = existing; targetCollection = existing;
break; break;
} }
} }
}
UUID targetCollectionId; UUID targetCollectionId;
if (targetCollection != null) { if (targetCollection != null) {
targetCollectionId = targetCollection.id; targetCollectionId = targetCollection.id;
if (!colDesc.isEmpty() && (targetCollection.description == null || targetCollection.description.isEmpty())) {
targetCollection.description = colDesc;
m_db.collectionDAO().save(targetCollection).blockingAwait();
}
} else { } else {
Collection newCol = new Collection(); Collection newCol = new Collection();
newCol.id = UUID.randomUUID(); newCol.id = colExportId != null ? colExportId : UUID.randomUUID();
newCol.name = colName; newCol.name = !colName.isEmpty() ? colName : "Imported Collection";
newCol.description = colDesc;
m_db.collectionDAO().save(newCol).blockingAwait(); m_db.collectionDAO().save(newCol).blockingAwait();
targetCollectionId = newCol.id; targetCollectionId = newCol.id;
existingCols.add(newCol);
} }
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString()) if (colExport.cards != null && !colExport.cards.isEmpty()) {
List<Monster> monstersToSave = new ArrayList<>();
Map<UUID, Integer> importedCardCounts = new HashMap<>();
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<Monster> existingColMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
.first(new ArrayList<>()).blockingGet(); .first(new ArrayList<>()).blockingGet();
Set<UUID> existingMonsterIds = new HashSet<>(); Map<UUID, Integer> existingCardCounts = new HashMap<>();
for (Monster m : colMonsters) { for (Monster m : existingColMonsters) {
existingMonsterIds.add(m.id); existingCardCounts.put(m.id, getCount(existingCardCounts, m.id) + 1);
} }
int ordinal = colMonsters.size(); int ordinal = existingColMonsters.size();
for (Monster monster : monstersToSave) { for (Map.Entry<UUID, Integer> entry : importedCardCounts.entrySet()) {
if (!existingMonsterIds.contains(monster.id)) { UUID monsterId = entry.getKey();
m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monster.id, ordinal++)).blockingAwait(); int importedCount = entry.getValue();
existingMonsterIds.add(monster.id); 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<Monster> dashboardMonstersToSave = new ArrayList<>();
Map<UUID, Integer> 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<DashboardMonsterWithMonster> existingDashboard = m_db.dashboardDAO().getDashboardMonstersWithMonster()
.first(new ArrayList<>()).blockingGet();
Map<UUID, Integer> 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<DashboardMonster> extraEntriesToAdd = new ArrayList<>();
for (Map.Entry<UUID, Integer> 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()); }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
} }
private static int getCount(Map<UUID, Integer> map, UUID key) {
if (map == null || key == null) return 0;
Integer val = map.get(key);
return val != null ? val : 0;
}
private static class Helpers { private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) { static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) { if (StringHelper.isNullOrEmpty(searchText)) {

View File

@@ -21,11 +21,23 @@ public enum AbilityScore {
} }
public static AbilityScore valueOfString(String string) { 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()) { for (AbilityScore abilityScore : values()) {
if (abilityScore.stringValue.equals(string)) { if (abilityScore.stringValue.equalsIgnoreCase(lower)
|| abilityScore.shortDisplayName.equalsIgnoreCase(lower)
|| abilityScore.name().equalsIgnoreCase(lower)) {
return abilityScore; 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; return AbilityScore.STRENGTH;
} }
} }

View File

@@ -1,34 +1,94 @@
package com.majinnaibu.monstercards.exporters; package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.majinnaibu.monstercards.models.BinderExport; import com.majinnaibu.monstercards.models.BinderExport;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
public class BinderExporter { public class BinderExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull @NonNull
public String exportBinder(String collectionName, List<Monster> monsters) { public String exportBinder(String collectionName, List<Monster> monsters) {
return exportBinder(new Collection(collectionName != null ? collectionName : "", ""), monsters, null);
}
@NonNull
public String exportBinder(@Nullable Collection collection, List<Monster> monsters) {
return exportBinder(collection, monsters, null);
}
@NonNull
public String exportBinder(@Nullable Collection collection, @Nullable List<Monster> monsters, @Nullable List<Monster> dashboardMonsters) {
BinderExport export = new BinderExport(); BinderExport export = new BinderExport();
export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
export.schemaVersion = 1; export.schemaVersion = 1;
if (monsters != null) { if (monsters != null) {
for (Monster monster : monsters) { for (Monster monster : monsters) {
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1; monster.schemaVersion = 1;
} }
} }
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( BinderExport.CollectionExport col = new BinderExport.CollectionExport(
collectionName != null ? collectionName : "", colId,
colName,
colDesc,
monsters monsters
); );
export.collections = Collections.singletonList(col); export.collections = Collections.singletonList(col);
}
return GSON.toJson(export);
}
@NonNull
public String exportFullBackup(@Nullable List<BinderExport.CollectionExport> collections, @Nullable List<Monster> 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); return GSON.toJson(export);
} }
} }

View File

@@ -11,6 +11,7 @@ public class MonsterCardExporter {
@NonNull @NonNull
public String exportCard(@NonNull Monster monster) { public String exportCard(@NonNull Monster monster) {
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1; monster.schemaVersion = 1;
return GSON.toJson(monster); return GSON.toJson(monster);
} }

View File

@@ -1,37 +1,83 @@
package com.majinnaibu.monstercards.helpers; package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull; 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.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.EntityImporter; import com.majinnaibu.monstercards.importers.EntityImporter;
import com.majinnaibu.monstercards.importers.MonsterJsonImporter; import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
import com.majinnaibu.monstercards.importers.Open5eImporter; import com.majinnaibu.monstercards.importers.Open5eImporter;
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter; import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
public class MonsterImportHelper { public class MonsterImportHelper {
private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>(); private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>();
static { static {
IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new MonsterJsonImporter()); IMPORTERS.add(new MonsterJsonImporter());
IMPORTERS.add(new Open5eImporter()); IMPORTERS.add(new Open5eImporter());
IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new DnDBeyondImporter()); IMPORTERS.add(new DnDBeyondImporter());
} }
@NonNull @NonNull
public static Monster fromJSON(String json) { public static Monster fromJSON(String json) {
if (json != null) { 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<Monster> importer : IMPORTERS) { for (EntityImporter<Monster> importer : IMPORTERS) {
if (importer.canImport(json)) { if (importer.canImport(json)) {
try { try {
return importer.parse(json); return importer.parse(json);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException("Failed to parse monster JSON", e); Logger.logError("Importer " + importer.getClass().getSimpleName() + " failed", e);
}
} }
} }
} }

View File

@@ -18,6 +18,12 @@ public class BinderImporter implements EntityImporter<BinderExport> {
JsonElement el = JsonParser.parseString(input); JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) { if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject(); 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(); return obj.has("collections") && obj.get("collections").isJsonArray();
} }
} catch (Exception ignored) { } catch (Exception ignored) {

View File

@@ -18,7 +18,13 @@ public class MonsterJsonImporter implements EntityImporter<Monster> {
JsonElement el = JsonParser.parseString(input); JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) { if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject(); 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) { } catch (Exception ignored) {
} }

View File

@@ -36,6 +36,12 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
return false; return false;
} }
JsonObject rootDict = element.getAsJsonObject(); 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") return rootDict.has("armorName") || rootDict.has("strPoints")
|| rootDict.has("isLegendary") || rootDict.has("isMythic") || rootDict.has("isLegendary") || rootDict.has("isMythic")
|| rootDict.has("legendariesDescription"); || rootDict.has("legendariesDescription");

View File

@@ -4,27 +4,46 @@ import com.google.gson.annotations.SerializedName;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
public class BinderExport { public class BinderExport {
@SerializedName("$schema")
public String schema = "https://majinnaibu.com/schemas/binder.schema.json";
@SerializedName("schemaVersion") @SerializedName("schemaVersion")
public int schemaVersion = 1; public int schemaVersion = 1;
@SerializedName("collections") @SerializedName("collections")
public List<CollectionExport> collections = new ArrayList<>(); public List<CollectionExport> collections = new ArrayList<>();
@SerializedName("dashboard")
public List<Monster> dashboard = new ArrayList<>();
public static class CollectionExport { public static class CollectionExport {
@SerializedName("id")
public String id;
@SerializedName("name") @SerializedName("name")
public String name; public String name;
@SerializedName("description")
public String description;
@SerializedName("cards") @SerializedName("cards")
public List<Monster> cards = new ArrayList<>(); public List<Monster> cards = new ArrayList<>();
public CollectionExport() { public CollectionExport() {
} }
public CollectionExport(String name, List<Monster> cards) { public CollectionExport(String id, String name, String description, List<Monster> cards) {
this.id = id != null ? id : UUID.randomUUID().toString();
this.name = name != null ? name : ""; this.name = name != null ? name : "";
this.description = description != null ? description : "";
this.cards = cards != null ? cards : new ArrayList<>(); this.cards = cards != null ? cards : new ArrayList<>();
} }
public CollectionExport(String name, List<Monster> cards) {
this(UUID.randomUUID().toString(), name, "", cards);
}
} }
} }

View File

@@ -9,6 +9,7 @@ import androidx.room.Entity;
import androidx.room.Ignore; import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import com.google.gson.annotations.SerializedName;
import com.majinnaibu.monstercards.data.enums.AbilityScore; import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType; import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType; import com.majinnaibu.monstercards.data.enums.ArmorType;
@@ -30,11 +31,16 @@ import java.util.UUID;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class Monster { public class Monster {
@SerializedName("$schema")
@Ignore
public String schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
@Ignore @Ignore
public int schemaVersion = 1; public int schemaVersion = 1;
@PrimaryKey @PrimaryKey
@NonNull @NonNull
@SerializedName("id")
public UUID id; public UUID id;
@NonNull @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) { switch (abilityScore) {
case STRENGTH: case STRENGTH:
return getStrengthModifier(); return getStrengthModifier();

View File

@@ -34,13 +34,25 @@ public class Skill implements Comparator<Skill>, Comparable<Skill> {
this.proficiencyType = proficiencyType; 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) { public int getSkillBonus(Monster monster) {
int modifier = monster.getAbilityModifier(abilityScore); AbilityScore score = getAbilityScore();
switch (proficiencyType) { int modifier = monster != null ? monster.getAbilityModifier(score) : 0;
ProficiencyType prof = proficiencyType != null ? proficiencyType : ProficiencyType.PROFICIENT;
switch (prof) {
case PROFICIENT: case PROFICIENT:
return modifier + monster.getProficiencyBonus(); return modifier + (monster != null ? monster.getProficiencyBonus() : 0);
case EXPERTISE: case EXPERTISE:
return modifier + monster.getProficiencyBonus() * 2; return modifier + (monster != null ? monster.getProficiencyBonus() * 2 : 0);
case NONE: case NONE:
default: default:
return modifier; return modifier;

View File

@@ -221,10 +221,32 @@ public class CollectionDetailFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_export_collection) { } else if (item.getItemId() == R.id.menu_action_export_collection) {
exportCollection(); exportCollection();
return true; return true;
} else if (item.getItemId() == R.id.menu_action_remove_all_monsters_from_collection) {
showRemoveAllMonstersConfirmationDialog();
return true;
} }
return super.onOptionsItemSelected(item); 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() { private void exportCollection() {
Collection collection = mViewModel.getCollection().getValue(); Collection collection = mViewModel.getCollection().getValue();
List<Monster> monsters = mViewModel.getMonsters().getValue(); List<Monster> monsters = mViewModel.getMonsters().getValue();

View File

@@ -4,6 +4,9 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
@@ -43,6 +46,7 @@ public class CollectionsFragment extends MCFragment {
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
View root = inflater.inflate(R.layout.fragment_collections, container, false); View root = inflater.inflate(R.layout.fragment_collections, container, false);
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection); FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
@@ -190,6 +194,51 @@ public class CollectionsFragment extends MCFragment {
Navigation.findNavController(requireView()).navigate(action); Navigation.findNavController(requireView()).navigate(action);
} }
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

View File

@@ -24,6 +24,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
@@ -265,7 +266,7 @@ public class DashboardFragment extends MCFragment {
private void showAddCollectionPicker() { private void showAddCollectionPicker() {
MonsterRepository repository = getMonsterRepository(); MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getCollections() mDisposables.add(repository.getCollectionsWithCount()
.firstOrError() .firstOrError()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@@ -279,12 +280,16 @@ public class DashboardFragment extends MCFragment {
} }
String[] names = new String[collections.size()]; String[] names = new String[collections.size()];
for (int i = 0; i < collections.size(); i++) { 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()) new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_add_collection_option) .setTitle(R.string.action_add_collection_option)
.setItems(names, (dialog, which) -> { .setItems(names, (dialog, which) -> {
Collection selected = collections.get(which); Collection selected = collections.get(which).collection;
addCollectionToDashboard(selected); addCollectionToDashboard(selected);
}) })
.setNegativeButton(R.string.dialog_cancel, null) .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) { } else if (item.getItemId() == R.id.menu_action_add_collection_option) {
showAddCollectionPicker(); showAddCollectionPicker();
return true; 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) { } else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
clearDashboard(); clearDashboard();
return true; return true;

View File

@@ -85,10 +85,35 @@ public class LibraryFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_export_library) { } else if (item.getItemId() == R.id.menu_action_export_library) {
exportLibrary(); exportLibrary();
return true; 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); 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() { private void exportLibrary() {
getMonsterRepository().getMonsters() getMonsterRepository().getMonsters()
.firstOrError() .firstOrError()
@@ -101,6 +126,16 @@ public class LibraryFragment extends MCFragment {
}, Logger::logError); }, 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) { private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
Context context = requireContext(); Context context = requireContext();
MonsterRepository repository = this.getMonsterRepository(); MonsterRepository repository = this.getMonsterRepository();

View File

@@ -40,6 +40,8 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -71,17 +73,22 @@ public class MCFragment extends Fragment {
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData(); Intent data = result.getData();
if (uri != null && getActivity() instanceof MainActivity) { List<Uri> uris = new ArrayList<>();
String content = readContentsFromUri(uri); if (data.getClipData() != null) {
if (content != null && !content.trim().isEmpty()) { ClipData clipData = data.getClipData();
((MainActivity) getActivity()).importMonsterFromInputAndNavigate(content); for (int i = 0; i < clipData.getItemCount(); i++) {
} else { Uri uri = clipData.getItemAt(i).getUri();
View view = getView(); if (uri != null) {
if (view != null) { uris.add(uri);
SnackbarHelper.showLong(view, R.string.failed_to_import_url);
} }
} }
} 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 intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
try { try {
mOpenDocumentLauncher.launch(intent); mOpenDocumentLauncher.launch(intent);
} catch (Exception e) { } catch (Exception e) {

View File

@@ -11,4 +11,9 @@
android:id="@+id/menu_action_export_collection" android:id="@+id/menu_action_export_collection"
android:title="@string/action_export_collection" android:title="@string/action_export_collection"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_action_remove_all_monsters_from_collection"
android:title="@string/action_remove_all"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_export_collections"
android:title="@string/action_export"
app:showAsAction="never" />
<item
android:id="@+id/menu_action_remove_all_collections"
android:title="@string/action_remove_all"
app:showAsAction="never" />
</menu>

View File

@@ -12,11 +12,6 @@
android:title="@string/action_add_collection_option" android:title="@string/action_add_collection_option"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_action_remove_single_monster"
android:title="@string/action_remove_single_monster"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_action_clear_dashboard" android:id="@+id/menu_action_clear_dashboard"
android:title="@string/action_clear_dashboard" android:title="@string/action_clear_dashboard"

View File

@@ -16,4 +16,14 @@
android:id="@+id/menu_action_export_library" android:id="@+id/menu_action_export_library"
android:title="@string/action_export_library" android:title="@string/action_export_library"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_action_export_everything"
android:title="@string/action_export_everything"
app:showAsAction="never" />
<item
android:id="@+id/menu_action_clear_all_data"
android:title="@string/action_clear_all_data"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -11,7 +11,7 @@
<string name="action_add_skill">Add Skill</string> <string name="action_add_skill">Add Skill</string>
<string name="action_add_trait">Add Trait</string> <string name="action_add_trait">Add Trait</string>
<string name="action_edit">Edit</string> <string name="action_edit">Edit</string>
<string name="action_import_monster">Import Monster</string> <string name="action_import_monster">Import File</string>
<string name="app_name">MonsterCards</string> <string name="app_name">MonsterCards</string>
<string name="charisma_abbreviation">CHA</string> <string name="charisma_abbreviation">CHA</string>
<string name="constitution_abbreviation">CON</string> <string name="constitution_abbreviation">CON</string>
@@ -133,7 +133,7 @@
<string name="title_editStrings">Strings</string> <string name="title_editStrings">Strings</string>
<string name="title_editTrait">Trait</string> <string name="title_editTrait">Trait</string>
<string name="title_editTraits">Traits</string> <string name="title_editTraits">Traits</string>
<string name="title_importMonster">Import Monster</string> <string name="title_importMonster">Import File</string>
<string name="title_library">Library</string> <string name="title_library">Library</string>
<string name="title_monsterDetails">Monster Details</string> <string name="title_monsterDetails">Monster Details</string>
<string name="title_monsterDetails_fmt">%1$s Details</string> <string name="title_monsterDetails_fmt">%1$s Details</string>
@@ -156,25 +156,25 @@
<string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string> <string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string>
<string name="action_add_single_monster">Add Monster</string> <string name="action_add_single_monster">Add Monster</string>
<string name="action_add_collection_option">Add Collection</string> <string name="action_add_collection_option">Add Collection</string>
<string name="action_clear_dashboard">Clear Dashboard</string> <string name="action_clear_dashboard">Clear</string>
<string name="dialog_add_to_dashboard">Add to Dashboard</string> <string name="dialog_add_to_dashboard">Add to Dashboard</string>
<string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string> <string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string>
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string> <string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
<string name="snackbar_dashboard_cleared">Dashboard cleared</string> <string name="snackbar_dashboard_cleared">Dashboard cleared</string>
<string name="no_monsters_available">No monsters available in library. Create one first!</string> <string name="no_monsters_available">No monsters available in library. Create one first!</string>
<string name="action_import_from_url">Import URL...</string> <string name="action_import_from_url">Import URL</string>
<string name="dialog_import_url_title">Import Entity or Character</string> <string name="dialog_import_url_title">Import Character</string>
<string name="dialog_import_url_message">Enter or paste a D&amp;D Beyond URL, Character ID, or Open5e JSON payload:</string> <string name="dialog_import_url_message">Enter or paste a D&amp;D Beyond URL, Character ID, or Open5e JSON payload:</string>
<string name="dialog_import_url_hint">e.g. D&amp;D Beyond URL or Open5e JSON</string> <string name="dialog_import_url_hint">e.g. D&amp;D Beyond URL or Open5e JSON</string>
<string name="dialog_import">Import</string> <string name="dialog_import">Import</string>
<string name="toast_importing_url">Importing entity...</string> <string name="toast_importing_url">Importing…</string>
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string> <string name="failed_to_import_url">Failed to import URL. Please check the URL, ID, or JSON format.</string>
<string name="action_share_monster">Share / Export Monster (.card)</string> <string name="action_share_monster">Share / Export Monster (.card)</string>
<string name="failed_to_share_monster">Failed to share monster</string> <string name="failed_to_share_monster">Failed to share monster</string>
<string name="action_export_card">Export Card</string> <string name="action_export_card">Export Card</string>
<string name="action_export_collection">Export Collection</string> <string name="action_export_collection">Export Collection</string>
<string name="action_export_library">Export Library</string> <string name="action_export_library">Export Library</string>
<string name="action_export_dashboard">Export Dashboard</string> <string name="action_export_dashboard">Export</string>
<string name="snackbar_export_success">Exported to %1$s</string> <string name="snackbar_export_success">Exported to %1$s</string>
<string name="snackbar_export_failed">Failed to export file</string> <string name="snackbar_export_failed">Failed to export file</string>
<string name="snackbar_import_binder_success">Imported binder successfully</string> <string name="snackbar_import_binder_success">Imported binder successfully</string>
@@ -189,7 +189,7 @@
<string name="empty_library_subtitle">Create new monster cards or import them from D&amp;D Beyond or Open5e.</string> <string name="empty_library_subtitle">Create new monster cards or import them from D&amp;D Beyond or Open5e.</string>
<string name="app_title_splash">Monster Cards</string> <string name="app_title_splash">Monster Cards</string>
<string name="action_remove_single_monster">Remove Monster...</string> <string name="action_remove_single_monster">Remove Monster</string>
<string name="action_remove">Remove</string> <string name="action_remove">Remove</string>
<string name="action_undo">Undo</string> <string name="action_undo">Undo</string>
<string name="action_view_details">View Details</string> <string name="action_view_details">View Details</string>
@@ -200,10 +200,29 @@
<string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string> <string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string>
<string name="action_create_monster">Create Monster</string> <string name="action_create_monster">Create Monster</string>
<string name="action_import_monster_from_url">Import Monster from Url</string> <string name="action_import_monster_from_url">Import URL</string>
<string name="action_import_monster_from_file">Import Monster from File</string> <string name="action_import_monster_from_file">Import File</string>
<string name="action_import_collection">Import Collection</string> <string name="action_import_collection">Import File</string>
<string name="title_library_actions">Library Actions</string> <string name="title_library_actions">Library Actions</string>
<string name="title_collection_actions">Collection Actions</string> <string name="title_collection_actions">Collection Actions</string>
<string name="title_dashboard_actions">Dashboard Actions</string> <string name="title_dashboard_actions">Dashboard Actions</string>
<string name="action_clear">Clear</string>
<string name="action_clear_all_data">Clear All Data</string>
<string name="dialog_clear_all_data_title">Clear All Data?</string>
<string name="dialog_clear_all_data_message">This will permanently delete all monsters, collections, and clear your dashboard. Are you sure?</string>
<string name="snackbar_all_data_cleared">All data cleared</string>
<string name="action_export_everything">Export All Data</string>
<string name="default_filename_export_everything">monster_cards_backup</string>
<string name="action_export">Export</string>
<string name="action_remove_all">Remove All</string>
<string name="dialog_remove_all_collections_title">Remove All Collections?</string>
<string name="dialog_remove_all_collections_message">Are you sure you want to remove all collections? Monsters in your library will not be deleted.</string>
<string name="snackbar_all_collections_removed">All collections removed</string>
<string name="dialog_remove_all_from_collection_title">Remove All Monsters?</string>
<string name="dialog_remove_all_from_collection_message">Are you sure you want to remove all monsters from this collection?</string>
<string name="snackbar_collection_cleared">Removed all monsters from collection</string>
</resources> </resources>

View File

@@ -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
}

514
monsters/baddies (1).binder Normal file
View File

@@ -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
}

514
monsters/baddies.binder Normal file
View File

@@ -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
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}