From 044202cb3a30ef64ca5011082c590266c8aa45f2 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Mon, 7 Sep 2026 18:16:08 -0700 Subject: [PATCH] Refactors monster importers to a common interface. --- .../helpers/MonsterImportHelper.java | 309 +----------------- .../importers/EntityImporter.java | 16 + .../importers/TetraCubeMonsterImporter.java | 305 +++++++++++++++++ .../TetraCubeMonsterImporterTest.java | 77 +++++ docs/Import-Export.md | 2 +- 5 files changed, 415 insertions(+), 294 deletions(-) create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/importers/EntityImporter.java create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java create mode 100644 Android/app/src/test/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporterTest.java 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 de7e0e8..b54bfa9 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 @@ -2,310 +2,33 @@ package com.majinnaibu.monstercards.helpers; import androidx.annotation.NonNull; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; -import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; -import com.majinnaibu.monstercards.data.enums.AbilityScore; -import com.majinnaibu.monstercards.data.enums.AdvantageType; -import com.majinnaibu.monstercards.data.enums.ProficiencyType; -import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.importers.EntityImporter; +import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter; import com.majinnaibu.monstercards.models.Monster; -import com.majinnaibu.monstercards.models.Skill; -import com.majinnaibu.monstercards.models.Trait; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Set; public class MonsterImportHelper { + private static final List> IMPORTERS = new ArrayList<>(); + + static { + IMPORTERS.add(new TetraCubeMonsterImporter()); + } + @NonNull public static Monster fromJSON(String json) { - JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject(); - - Monster monster = new Monster(); - monster.name = Helpers.getString(rootDict, "name"); - monster.size = Helpers.getString(rootDict, "size"); - monster.type = Helpers.getString(rootDict, "type"); - monster.subtype = Helpers.getString(rootDict, "tag"); - monster.alignment = Helpers.getString(rootDict, "alignment"); - monster.hitDice = Helpers.getInt(rootDict, "hitDice"); - monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(Helpers.getString(rootDict, "armorName")); - monster.shieldBonus = Helpers.getInt(rootDict, "shieldBonus"); - monster.naturalArmorBonus = Helpers.getInt(rootDict, "natArmorBonus"); - monster.otherArmorDescription = Helpers.getString(rootDict, "otherArmorDesc"); - monster.walkSpeed = Helpers.getInt(rootDict, "speed"); - monster.burrowSpeed = Helpers.getInt(rootDict, "burrowSpeed"); - monster.climbSpeed = Helpers.getInt(rootDict, "climbSpeed"); - monster.flySpeed = Helpers.getInt(rootDict, "flySpeed"); - monster.canHover = Helpers.getBool(rootDict, "hover"); - monster.swimSpeed = Helpers.getInt(rootDict, "swimSpeed"); - monster.hasCustomHP = Helpers.getBool(rootDict, "customHP"); - monster.hasCustomSpeed = Helpers.getBool(rootDict, "customSpeed"); - monster.customHPDescription = Helpers.getString(rootDict, "hpText"); - monster.customSpeedDescription = Helpers.getString(rootDict, "speedDesc"); - monster.strengthScore = Helpers.getInt(rootDict, "strPoints"); - monster.dexterityScore = Helpers.getInt(rootDict, "dexPoints"); - monster.constitutionScore = Helpers.getInt(rootDict, "conPoints"); - monster.intelligenceScore = Helpers.getInt(rootDict, "intPoints"); - monster.wisdomScore = Helpers.getInt(rootDict, "wisPoints"); - monster.charismaScore = Helpers.getInt(rootDict, "chaPoints"); - Helpers.addSense(monster, rootDict, "blindsight"); - // Helpers.getBool(rootDict, "blind"); - Helpers.addSense(monster, rootDict, "darkvision"); - Helpers.addSense(monster, rootDict, "tremorsense"); - Helpers.addSense(monster, rootDict, "truesight"); - monster.telepathyRange = Helpers.getInt(rootDict, "telepathy"); - monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(Helpers.getString(rootDict, "cr")); - monster.customChallengeRatingDescription = Helpers.getString(rootDict, "customCr"); - monster.customProficiencyBonus = Helpers.getInt(rootDict, "customProf"); - // Helpers.getBool(rootDict, "isLegendary"); - // Helpers.getString(rootDict, "legendariesDescription"); - // Helpers.getBool(rootDict, "isLair"); - // Helpers.getString(rootDict, "lairDescription"); - // Helpers.getString(rootDict, "lairDescriptionEnd"); - // Helpers.getBool(rootDict, "isRegional"); - // Helpers.getString(rootDict, "regionalDescription"); - // Helpers.getString(rootDict, "regionalDescriptionEnd"); - // properties: [] - monster.abilities = Helpers.getListOfTraits(rootDict, "abilities"); - monster.actions = Helpers.getListOfTraits(rootDict, "actions"); - monster.reactions = Helpers.getListOfTraits(rootDict, "reactions"); - monster.legendaryActions = Helpers.getListOfTraits(rootDict, "legendaries"); - monster.lairActions = Helpers.getListOfTraits(rootDict, "lairs"); - monster.regionalActions = Helpers.getListOfTraits(rootDict, "regionals"); - Helpers.addSavingThrows(monster, rootDict); - // skills: [] - monster.skills = Helpers.getSetOfSkills(rootDict); - // damagetypes: [] - // specialdamage: [] - monster.damageImmunities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "i"); - monster.damageImmunities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "i")); - monster.damageResistances = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "r"); - monster.damageResistances.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "r")); - monster.damageVulnerabilities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "v"); - monster.damageVulnerabilities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "v")); - // conditions: [] - monster.conditionImmunities = Helpers.getSetOfDamageTypes(rootDict, "conditions"); - // languages: [] - monster.languages = Helpers.getSetOfLanguages(rootDict, "languages"); - // understandsBut: "" - monster.understandsButDescription = Helpers.getString(rootDict, "understandsBut"); - // shortName: "" - // doubleColumns: true - // separationPoint: -1 - // damage: [] - // pluralName: "" - - return monster; - } - - - public static class Helpers { - public static String getString(JsonObject dict, String name) { - return getString(dict, name, ""); - } - - public static String getString(@NonNull JsonObject dict, String name, String defaultValue) { - if (dict.has(name)) { - return dict.get(name).getAsString(); - } - - return defaultValue; - } - - public static int getInt(JsonObject dict, String name) { - return getInt(dict, name, 0); - } - - public static int getInt(@NonNull JsonObject dict, String name, int defaultValue) { - if (dict.has(name)) { - JsonElement element = dict.get(name); - if (element.isJsonPrimitive()) { - JsonPrimitive rawValue = element.getAsJsonPrimitive(); - if (rawValue.isNumber()) { - return rawValue.getAsInt(); - } else { - try { - return rawValue.getAsInt(); - } catch (Exception ex) { - return defaultValue; - } - } - } - } - return defaultValue; - } - - public static boolean getBool(JsonObject dict, String name) { - return getBool(dict, name, false); - } - - public static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) { - if (dict.has(name)) { - JsonElement element = dict.get(name); - if (element.isJsonPrimitive()) { - JsonPrimitive rawValue = element.getAsJsonPrimitive(); - if (rawValue.isBoolean()) { - return rawValue.getAsBoolean(); - } else { - try { - return rawValue.getAsBoolean(); - } catch (Exception ex) { - return defaultValue; - } - } - } - } - return defaultValue; - } - - @NonNull - public static String formatDistance(String name, int distance) { - // TODO: consider moving this to a string resource so it can be localized - return String.format(Locale.getDefault(), "%s %d ft.", name, distance); - } - - public static void addSense(Monster monster, JsonObject root, String name) { - int distance = Helpers.getInt(root, name); - if (distance > 0) { - monster.senses.add(Helpers.formatDistance(name, distance)); - } - } - - @NonNull - public static List getListOfTraits(@NonNull JsonObject dict, String name) { - ArrayList traits = new ArrayList<>(); - if (dict.has(name)) { - JsonElement arrayElement = dict.get(name); - if (arrayElement.isJsonArray()) { - JsonArray array = arrayElement.getAsJsonArray(); - int size = array.size(); - for (int index = 0; index < size; index++) { - JsonElement jsonElement = array.get(index); - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - String traitName = Helpers.getString(jsonObject, "name"); - String description = Helpers.getString(jsonObject, "desc"); - Trait trait = new Trait(traitName, description); - traits.add(trait); - } - } - } - } - return traits; - } - - public static void addSavingThrows(Monster monster, @NonNull JsonObject root) { - if (root.has("sthrows")) { - JsonElement arrayElement = root.get("sthrows"); - if (arrayElement.isJsonArray()) { - JsonArray array = arrayElement.getAsJsonArray(); - int size = array.size(); - for (int index = 0; index < size; index++) { - JsonElement jsonElement = array.get(index); - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - String name = Helpers.getString(jsonObject, "name"); - if ("str".equals(name)) { - monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT; - } else if ("dex".equals(name)) { - monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT; - } else if ("con".equals(name)) { - monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT; - } else if ("int".equals(name)) { - monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT; - } else if ("wis".equals(name)) { - monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT; - } else if ("cha".equals(name)) { - monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT; - } - } + 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); } } } } - - @NonNull - public static Set getSetOfSkills(@NonNull JsonObject root) { - HashSet skills = new HashSet<>(); - if (root.has("skills")) { - JsonElement arrayElement = root.get("skills"); - if (arrayElement.isJsonArray()) { - JsonArray array = arrayElement.getAsJsonArray(); - int size = array.size(); - for (int index = 0; index < size; index++) { - JsonElement jsonElement = array.get(index); - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - String name = Helpers.getString(jsonObject, "name"); - String stat = Helpers.getString(jsonObject, "stat"); - String note = Helpers.getString(jsonObject, "note"); - - Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT); - skills.add(skill); - } - } - } - } - return skills; - } - - @NonNull - public static Set getSetOfDamageTypes(JsonObject rootDict, String name) { - return getSetOfDamageTypes(rootDict, name, null); - } - - @NonNull - public static Set getSetOfDamageTypes(@NonNull JsonObject root, String name, String type) { - HashSet damageTypes = new HashSet<>(); - if (root.has(name)) { - JsonElement arrayElement = root.get(name); - if (arrayElement.isJsonArray()) { - JsonArray array = arrayElement.getAsJsonArray(); - int size = array.size(); - for (int index = 0; index < size; index++) { - JsonElement jsonElement = array.get(index); - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - String dtName = Helpers.getString(jsonObject, "name"); - String dtType = Helpers.getString(jsonObject, "type"); - if (type == null || type.equals(dtType)) { - damageTypes.add(dtName); - } - } - } - } - } - return damageTypes; - } - - @NonNull - public static Set getSetOfLanguages(@NonNull JsonObject root, String name) { - HashSet languages = new HashSet<>(); - if (root.has(name)) { - JsonElement arrayElement = root.get(name); - if (arrayElement.isJsonArray()) { - JsonArray array = arrayElement.getAsJsonArray(); - int size = array.size(); - for (int index = 0; index < size; index++) { - JsonElement jsonElement = array.get(index); - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - String languageName = Helpers.getString(jsonObject, "name"); - boolean canSpeak = Helpers.getBool(jsonObject, "speaks"); - Language language = new Language(languageName, canSpeak); - languages.add(language); - } - } - } - } - return languages; - } + 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/EntityImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/EntityImporter.java new file mode 100644 index 0000000..f1498d2 --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/EntityImporter.java @@ -0,0 +1,16 @@ +package com.majinnaibu.monstercards.importers; + +import androidx.annotation.NonNull; + +public interface EntityImporter { + /** + * Determines whether the given input string or payload can be handled by this importer. + */ + boolean canImport(@NonNull String input); + + /** + * Parses the raw input string into internal domain memory objects (e.g., Monster). + */ + @NonNull + T parse(@NonNull String input) throws Exception; +} 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 new file mode 100644 index 0000000..228d78b --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java @@ -0,0 +1,305 @@ +package com.majinnaibu.monstercards.importers; + +import androidx.annotation.NonNull; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; +import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.models.Trait; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class TetraCubeMonsterImporter implements EntityImporter { + + @Override + public boolean canImport(@NonNull String input) { + if (input.trim().isEmpty()) { + return false; + } + try { + JsonElement element = JsonParser.parseString(input); + if (!element.isJsonObject()) { + return false; + } + JsonObject rootDict = element.getAsJsonObject(); + return rootDict.has("hitDice") || rootDict.has("armorName") || rootDict.has("strPoints"); + } catch (Exception e) { + return false; + } + } + + @NonNull + @Override + public Monster parse(@NonNull String input) throws Exception { + JsonObject rootDict = JsonParser.parseString(input).getAsJsonObject(); + + Monster monster = new Monster(); + monster.name = getString(rootDict, "name"); + monster.size = getString(rootDict, "size"); + monster.type = getString(rootDict, "type"); + monster.subtype = getString(rootDict, "tag"); + monster.alignment = getString(rootDict, "alignment"); + monster.hitDice = getInt(rootDict, "hitDice"); + monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(getString(rootDict, "armorName")); + monster.shieldBonus = getInt(rootDict, "shieldBonus"); + monster.naturalArmorBonus = getInt(rootDict, "natArmorBonus"); + monster.otherArmorDescription = getString(rootDict, "otherArmorDesc"); + monster.walkSpeed = getInt(rootDict, "speed"); + monster.burrowSpeed = getInt(rootDict, "burrowSpeed"); + monster.climbSpeed = getInt(rootDict, "climbSpeed"); + monster.flySpeed = getInt(rootDict, "flySpeed"); + monster.canHover = getBool(rootDict, "hover"); + monster.swimSpeed = getInt(rootDict, "swimSpeed"); + monster.hasCustomHP = getBool(rootDict, "customHP"); + monster.hasCustomSpeed = getBool(rootDict, "customSpeed"); + monster.customHPDescription = getString(rootDict, "hpText"); + monster.customSpeedDescription = getString(rootDict, "speedDesc"); + monster.strengthScore = getInt(rootDict, "strPoints"); + monster.dexterityScore = getInt(rootDict, "dexPoints"); + monster.constitutionScore = getInt(rootDict, "conPoints"); + monster.intelligenceScore = getInt(rootDict, "intPoints"); + monster.wisdomScore = getInt(rootDict, "wisPoints"); + monster.charismaScore = getInt(rootDict, "chaPoints"); + addSense(monster, rootDict, "blindsight"); + addSense(monster, rootDict, "darkvision"); + addSense(monster, rootDict, "tremorsense"); + addSense(monster, rootDict, "truesight"); + monster.telepathyRange = getInt(rootDict, "telepathy"); + monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(getString(rootDict, "cr")); + monster.customChallengeRatingDescription = getString(rootDict, "customCr"); + monster.customProficiencyBonus = getInt(rootDict, "customProf"); + + monster.abilities = getListOfTraits(rootDict, "abilities"); + monster.actions = getListOfTraits(rootDict, "actions"); + monster.reactions = getListOfTraits(rootDict, "reactions"); + monster.legendaryActions = getListOfTraits(rootDict, "legendaries"); + monster.lairActions = getListOfTraits(rootDict, "lairs"); + monster.regionalActions = getListOfTraits(rootDict, "regionals"); + addSavingThrows(monster, rootDict); + monster.skills = getSetOfSkills(rootDict); + monster.damageImmunities = getSetOfDamageTypes(rootDict, "damageTypes", "i"); + monster.damageImmunities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "i")); + monster.damageResistances = getSetOfDamageTypes(rootDict, "damageTypes", "r"); + monster.damageResistances.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "r")); + monster.damageVulnerabilities = getSetOfDamageTypes(rootDict, "damageTypes", "v"); + monster.damageVulnerabilities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "v")); + monster.conditionImmunities = getSetOfDamageTypes(rootDict, "conditions"); + monster.languages = getSetOfLanguages(rootDict, "languages"); + monster.understandsButDescription = getString(rootDict, "understandsBut"); + + return monster; + } + + private static String getString(JsonObject dict, String name) { + return getString(dict, name, ""); + } + + private static String getString(@NonNull JsonObject dict, String name, String defaultValue) { + if (dict.has(name)) { + return dict.get(name).getAsString(); + } + return defaultValue; + } + + private static int getInt(JsonObject dict, String name) { + return getInt(dict, name, 0); + } + + private static int getInt(@NonNull JsonObject dict, String name, int defaultValue) { + if (dict.has(name)) { + JsonElement element = dict.get(name); + if (element.isJsonPrimitive()) { + JsonPrimitive rawValue = element.getAsJsonPrimitive(); + if (rawValue.isNumber()) { + return rawValue.getAsInt(); + } else { + try { + return rawValue.getAsInt(); + } catch (Exception ex) { + return defaultValue; + } + } + } + } + return defaultValue; + } + + private static boolean getBool(JsonObject dict, String name) { + return getBool(dict, name, false); + } + + private static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) { + if (dict.has(name)) { + JsonElement element = dict.get(name); + if (element.isJsonPrimitive()) { + JsonPrimitive rawValue = element.getAsJsonPrimitive(); + if (rawValue.isBoolean()) { + return rawValue.getAsBoolean(); + } else { + try { + return rawValue.getAsBoolean(); + } catch (Exception ex) { + return defaultValue; + } + } + } + } + return defaultValue; + } + + @NonNull + private static String formatDistance(String name, int distance) { + return String.format(Locale.getDefault(), "%s %d ft.", name, distance); + } + + private static void addSense(Monster monster, JsonObject root, String name) { + int distance = getInt(root, name); + if (distance > 0) { + monster.senses.add(formatDistance(name, distance)); + } + } + + @NonNull + private static List getListOfTraits(@NonNull JsonObject dict, String name) { + ArrayList traits = new ArrayList<>(); + if (dict.has(name)) { + JsonElement arrayElement = dict.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String traitName = getString(jsonObject, "name"); + String description = getString(jsonObject, "desc"); + Trait trait = new Trait(traitName, description); + traits.add(trait); + } + } + } + } + return traits; + } + + private static void addSavingThrows(Monster monster, @NonNull JsonObject root) { + if (root.has("sthrows")) { + JsonElement arrayElement = root.get("sthrows"); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String name = getString(jsonObject, "name"); + if ("str".equals(name)) { + monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("dex".equals(name)) { + monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("con".equals(name)) { + monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("int".equals(name)) { + monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("wis".equals(name)) { + monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("cha".equals(name)) { + monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT; + } + } + } + } + } + } + + @NonNull + private static Set getSetOfSkills(@NonNull JsonObject root) { + HashSet skills = new HashSet<>(); + if (root.has("skills")) { + JsonElement arrayElement = root.get("skills"); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String name = getString(jsonObject, "name"); + String stat = getString(jsonObject, "stat"); + String note = getString(jsonObject, "note"); + + Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT); + skills.add(skill); + } + } + } + } + return skills; + } + + @NonNull + private static Set getSetOfDamageTypes(JsonObject rootDict, String name) { + return getSetOfDamageTypes(rootDict, name, null); + } + + @NonNull + private static Set getSetOfDamageTypes(JsonObject rootDict, String name, String type) { + HashSet damageTypes = new HashSet<>(); + if (rootDict.has(name)) { + JsonElement arrayElement = rootDict.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String dtName = getString(jsonObject, "name"); + String dtType = getString(jsonObject, "type"); + if (type == null || type.equals(dtType)) { + damageTypes.add(dtName); + } + } + } + } + } + return damageTypes; + } + + @NonNull + private static Set getSetOfLanguages(@NonNull JsonObject root, String name) { + HashSet languages = new HashSet<>(); + if (root.has(name)) { + JsonElement arrayElement = root.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String languageName = getString(jsonObject, "name"); + boolean canSpeak = getBool(jsonObject, "speaks"); + Language language = new Language(languageName, canSpeak); + languages.add(language); + } + } + } + } + return languages; + } +} diff --git a/Android/app/src/test/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporterTest.java b/Android/app/src/test/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporterTest.java new file mode 100644 index 0000000..0509966 --- /dev/null +++ b/Android/app/src/test/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporterTest.java @@ -0,0 +1,77 @@ +package com.majinnaibu.monstercards.importers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.majinnaibu.monstercards.helpers.MonsterImportHelper; +import com.majinnaibu.monstercards.models.Monster; + +import org.junit.Test; + +public class TetraCubeMonsterImporterTest { + + private final TetraCubeMonsterImporter importer = new TetraCubeMonsterImporter(); + + private static final String SAMPLE_TETRACUBE_JSON = "{" + + "\"name\": \"Goblin\"," + + "\"size\": \"Small\"," + + "\"type\": \"humanoid\"," + + "\"tag\": \"goblinoid\"," + + "\"alignment\": \"neutral evil\"," + + "\"hitDice\": 2," + + "\"armorName\": \"leather\"," + + "\"shieldBonus\": 1," + + "\"speed\": 30," + + "\"strPoints\": 8," + + "\"dexPoints\": 14," + + "\"conPoints\": 10," + + "\"intPoints\": 10," + + "\"wisPoints\": 8," + + "\"chaPoints\": 8," + + "\"cr\": \"1/4\"" + + "}"; + + @Test + public void canImport_validTetraCubeJson_returnsTrue() { + assertTrue(importer.canImport(SAMPLE_TETRACUBE_JSON)); + } + + @Test + public void canImport_invalidJson_returnsFalse() { + assertFalse(importer.canImport("not a json string")); + } + + @Test + public void canImport_emptyString_returnsFalse() { + assertFalse(importer.canImport("")); + } + + @Test + public void canImport_otherJson_returnsFalse() { + assertFalse(importer.canImport("{\"foo\": \"bar\"}")); + } + + @Test + public void parse_validTetraCubeJson_createsMonster() throws Exception { + Monster monster = importer.parse(SAMPLE_TETRACUBE_JSON); + assertNotNull(monster); + assertEquals("Goblin", monster.name); + assertEquals("Small", monster.size); + assertEquals("humanoid", monster.type); + assertEquals("goblinoid", monster.subtype); + assertEquals("neutral evil", monster.alignment); + assertEquals(2, monster.hitDice); + assertEquals(30, monster.walkSpeed); + assertEquals(8, monster.strengthScore); + assertEquals(14, monster.dexterityScore); + } + + @Test + public void monsterImportHelper_fromJSON_delegatesToImporter() { + Monster monster = MonsterImportHelper.fromJSON(SAMPLE_TETRACUBE_JSON); + assertNotNull(monster); + assertEquals("Goblin", monster.name); + } +} diff --git a/docs/Import-Export.md b/docs/Import-Export.md index b951597..5273491 100644 --- a/docs/Import-Export.md +++ b/docs/Import-Export.md @@ -168,7 +168,7 @@ Extend the generic sharing feature with specialized, direct sharing channels (to ## 3. Implementation Checklist & Status - [x] **Step 0**: Restrict app intent filters in `AndroidManifest.xml` (`.monster` & `.monster.txt`) and add runtime filename validation in `MainActivity.java`. -- [ ] **Step 1**: Refactor import & conversion code into a shared `EntityImporter` interface and `TetraCubeMonsterImporter` class. +- [x] **Step 1**: Refactor import & conversion code into a shared `EntityImporter` interface and `TetraCubeMonsterImporter` class. - [ ] **Step 2**: Update Tetra-cube importer class to support the newest Tetra-cube format (`bonusActions`, `mythics`, `blind`, intro descriptions). - [ ] **Step 3**: Import from D&D Beyond URL (`https://www.dndbeyond.com/characters/49074997` fetching from character service endpoint `character/v2/character/49074997`). - [ ] **Step 4**: Export to internal format described by Open5e document (`Open5eExporter`).