Adds import support from our other project.

This commit is contained in:
2026-09-07 21:57:59 -07:00
parent a3d1a139e9
commit a3dace36de
150 changed files with 17716 additions and 21 deletions

View File

@@ -35,7 +35,7 @@
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- Intent filter for file:// URIs ending in .monster or .monster.txt -->
<!-- Intent filter for file:// URIs ending in .monster, .monster.txt, .card, or .card.txt -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
@@ -46,6 +46,8 @@
<data android:scheme="file" android:host="*" />
<data android:pathPattern=".*\\.monster" />
<data android:pathPattern=".*\\.monster\\.txt" />
<data android:pathPattern=".*\\.card" />
<data android:pathPattern=".*\\.card\\.txt" />
</intent-filter>
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->

View File

@@ -20,7 +20,7 @@ import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.MonsterFTS;
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 6)
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 8)
@TypeConverters({
ArmorTypeConverter.class,
ChallengeRatingConverter.class,

View File

@@ -143,7 +143,7 @@ public class MainActivity extends AppCompatActivity {
if (uri == null || !isMonsterFile(uri)) {
if (uri != null) {
Logger.logError("Ignored file because extension is not .monster or .monster.txt: " + uri);
Logger.logError("Ignored file because extension is not supported (.monster, .card): " + uri);
}
return null;
}
@@ -161,7 +161,8 @@ public class MainActivity extends AppCompatActivity {
return false;
}
String lowerName = fileName.toLowerCase(Locale.ROOT);
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt");
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt");
}
@Nullable

View File

@@ -66,6 +66,35 @@ public class MonsterCardsApplication extends Application {
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions_description` TEXT DEFAULT ''");
}
};
private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE monsters ADD COLUMN `player_name` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `background` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `personality_traits` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `ideals` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonds` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `flaws` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `age` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `height` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `weight` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `eyes` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `skin` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `hair` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `appearance` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `backstory` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `allies_and_organizations` TEXT NOT NULL DEFAULT ''");
}
};
private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS `new_monsters` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
database.execSQL("INSERT INTO `new_monsters` SELECT id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions, source_url, bonus_actions, mythic_actions, legendary_actions_description, lair_actions_description, lair_actions_end_note, regional_actions_description, regional_actions_end_note, mythic_actions_description, COALESCE(player_name, ''), COALESCE(background, ''), COALESCE(personality_traits, ''), COALESCE(ideals, ''), COALESCE(bonds, ''), COALESCE(flaws, ''), COALESCE(age, ''), COALESCE(height, ''), COALESCE(weight, ''), COALESCE(eyes, ''), COALESCE(skin, ''), COALESCE(hair, ''), COALESCE(appearance, ''), COALESCE(backstory, ''), COALESCE(allies_and_organizations, '') FROM `monsters`");
database.execSQL("DROP TABLE `monsters`");
database.execSQL("ALTER TABLE `new_monsters` RENAME TO `monsters`");
}
};
private MonsterRepository m_monsterLibraryRepository;
@@ -90,6 +119,8 @@ public class MonsterCardsApplication extends Application {
.addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.fallbackToDestructiveMigrationOnDowngrade()
// .fallbackToDestructiveMigration()
.build();

View File

@@ -0,0 +1,287 @@
package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.ArmorType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class Open5eExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull
public String exportMonster(@NonNull Monster monster) {
JsonObject root = new JsonObject();
root.addProperty("$schema", "../../../schema/entity.json");
root.addProperty("uuid", monster.id != null ? monster.id.toString() : UUID.randomUUID().toString());
root.addProperty("ruleset_id", "open5e");
root.addProperty("entity_type", "character");
root.addProperty("display_name", monster.name != null && !monster.name.isEmpty() ? monster.name : "Unnamed Entity");
root.addProperty("description", buildDescription(monster));
root.addProperty("version", "1.0.0");
root.addProperty("template_id", "stat_block");
JsonObject props = new JsonObject();
props.addProperty("name", monster.name != null ? monster.name : "");
props.addProperty("size", monster.size != null ? monster.size : "");
props.addProperty("type", monster.type != null ? monster.type : "");
props.addProperty("subtype", monster.subtype != null ? monster.subtype : "");
props.addProperty("alignment", monster.alignment != null ? monster.alignment : "");
props.addProperty("armor_class", monster.getArmorClassValue());
props.addProperty("armor_description", monster.getArmorClass());
if (monster.armorType != null && !monster.armorType.equals(ArmorType.NONE)) {
JsonObject armorObj = new JsonObject();
armorObj.addProperty("id", monster.armorType.stringValue);
armorObj.addProperty("name", monster.armorType.displayName);
armorObj.addProperty("armor_category", getArmorCategory(monster.armorType));
armorObj.addProperty("ac_base", monster.armorType.baseArmorClass);
Integer dexCap = getDexCap(monster.armorType);
if (dexCap != null) {
armorObj.addProperty("dex_cap", dexCap);
} else {
armorObj.add("dex_cap", JsonNull.INSTANCE);
}
armorObj.addProperty("stealth_disadvantage", isStealthDisadvantage(monster.armorType));
props.add("equipped_armor", armorObj);
} else {
props.add("equipped_armor", JsonNull.INSTANCE);
}
if (monster.shieldBonus > 0) {
JsonObject shieldObj = new JsonObject();
shieldObj.addProperty("id", "shield");
shieldObj.addProperty("name", "Shield");
shieldObj.addProperty("ac_bonus", monster.shieldBonus);
props.add("equipped_shield", shieldObj);
} else {
props.add("equipped_shield", JsonNull.INSTANCE);
}
JsonObject hpObj = new JsonObject();
hpObj.addProperty("max", monster.getHitPointsValue());
hpObj.addProperty("current", monster.getHitPointsValue());
hpObj.addProperty("formula", monster.hasCustomHP && !monster.customHPDescription.isEmpty() ? monster.customHPDescription : monster.getHitPoints());
hpObj.addProperty("hit_dice", monster.hitDice + "d8");
props.add("hit_points", hpObj);
props.addProperty("speed", monster.walkSpeed);
props.addProperty("speed_desc", monster.getSpeedText());
props.addProperty("challenge_rating", monster.getChallengeRatingDescription());
props.addProperty("cr", monster.challengeRating != null ? monster.challengeRating.stringValue : "1");
JsonObject abilitiesObj = new JsonObject();
abilitiesObj.addProperty("strength", monster.strengthScore);
abilitiesObj.addProperty("dexterity", monster.dexterityScore);
abilitiesObj.addProperty("constitution", monster.constitutionScore);
abilitiesObj.addProperty("intelligence", monster.intelligenceScore);
abilitiesObj.addProperty("wisdom", monster.wisdomScore);
abilitiesObj.addProperty("charisma", monster.charismaScore);
props.add("abilities", abilitiesObj);
if (monster.playerName != null && !monster.playerName.isEmpty()) props.addProperty("player_name", monster.playerName);
if (monster.background != null && !monster.background.isEmpty()) props.addProperty("background", monster.background);
if (monster.personalityTraits != null && !monster.personalityTraits.isEmpty()) props.addProperty("personality_traits", monster.personalityTraits);
if (monster.ideals != null && !monster.ideals.isEmpty()) props.addProperty("ideals", monster.ideals);
if (monster.bonds != null && !monster.bonds.isEmpty()) props.addProperty("bonds", monster.bonds);
if (monster.flaws != null && !monster.flaws.isEmpty()) props.addProperty("flaws", monster.flaws);
if (hasDetails(monster)) {
JsonObject detailsObj = new JsonObject();
if (monster.age != null && !monster.age.isEmpty()) detailsObj.addProperty("age", monster.age);
if (monster.height != null && !monster.height.isEmpty()) detailsObj.addProperty("height", monster.height);
if (monster.weight != null && !monster.weight.isEmpty()) detailsObj.addProperty("weight", monster.weight);
if (monster.eyes != null && !monster.eyes.isEmpty()) detailsObj.addProperty("eyes", monster.eyes);
if (monster.skin != null && !monster.skin.isEmpty()) detailsObj.addProperty("skin", monster.skin);
if (monster.hair != null && !monster.hair.isEmpty()) detailsObj.addProperty("hair", monster.hair);
if (monster.appearance != null && !monster.appearance.isEmpty()) detailsObj.addProperty("appearance", monster.appearance);
if (monster.backstory != null && !monster.backstory.isEmpty()) detailsObj.addProperty("backstory", monster.backstory);
if (monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty()) detailsObj.addProperty("allies_and_organizations", monster.alliesAndOrganizations);
props.add("character_details", detailsObj);
}
JsonArray savesArr = buildSavingThrowsArray(monster);
if (savesArr.size() > 0) props.add("saving_throws", savesArr);
JsonArray skillsArr = buildSkillsArray(monster);
if (skillsArr.size() > 0) props.add("skills", skillsArr);
if (monster.damageImmunities != null && !monster.damageImmunities.isEmpty()) props.add("damage_immunities", toJsonArray(monster.damageImmunities));
if (monster.damageResistances != null && !monster.damageResistances.isEmpty()) props.add("damage_resistances", toJsonArray(monster.damageResistances));
if (monster.damageVulnerabilities != null && !monster.damageVulnerabilities.isEmpty()) props.add("damage_vulnerabilities", toJsonArray(monster.damageVulnerabilities));
if (monster.conditionImmunities != null && !monster.conditionImmunities.isEmpty()) props.add("condition_immunities", toJsonArray(monster.conditionImmunities));
props.addProperty("senses", monster.getSensesDescription());
if (monster.languages != null && !monster.languages.isEmpty()) {
JsonArray langArr = new JsonArray();
for (Language l : monster.languages) {
if (l != null && l.getName() != null && !l.getName().isEmpty()) {
langArr.add(l.getName());
}
}
props.add("languages", langArr);
}
if (monster.abilities != null && !monster.abilities.isEmpty()) props.add("traits", traitsToJsonArray(monster.abilities));
if (monster.actions != null && !monster.actions.isEmpty()) props.add("actions", traitsToJsonArray(monster.actions));
if (monster.bonusActions != null && !monster.bonusActions.isEmpty()) props.add("bonus_actions", traitsToJsonArray(monster.bonusActions));
if (monster.reactions != null && !monster.reactions.isEmpty()) props.add("reactions", traitsToJsonArray(monster.reactions));
if (monster.legendaryActions != null && !monster.legendaryActions.isEmpty()) props.add("legendary_actions", traitsToJsonArray(monster.legendaryActions));
if (monster.legendaryActionsDescription != null && !monster.legendaryActionsDescription.isEmpty()) props.addProperty("legendary_desc", monster.legendaryActionsDescription);
if (monster.lairActions != null && !monster.lairActions.isEmpty()) props.add("lair_actions", traitsToJsonArray(monster.lairActions));
if (monster.lairActionsDescription != null && !monster.lairActionsDescription.isEmpty()) props.addProperty("lair_desc", monster.lairActionsDescription);
if (monster.regionalActions != null && !monster.regionalActions.isEmpty()) props.add("regional_effects", traitsToJsonArray(monster.regionalActions));
if (monster.regionalActionsDescription != null && !monster.regionalActionsDescription.isEmpty()) props.addProperty("regional_desc", monster.regionalActionsDescription);
if (monster.mythicActions != null && !monster.mythicActions.isEmpty()) props.add("mythic_actions", traitsToJsonArray(monster.mythicActions));
if (monster.mythicActionsDescription != null && !monster.mythicActionsDescription.isEmpty()) props.addProperty("mythic_desc", monster.mythicActionsDescription);
if (monster.sourceUrl != null && !monster.sourceUrl.isEmpty()) props.addProperty("source_url", monster.sourceUrl);
root.add("properties", props);
return GSON.toJson(root);
}
private String getArmorCategory(ArmorType armorType) {
switch (armorType) {
case PADDED:
case LEATHER:
case STUDDED_LEATHER:
return "light";
case HIDE:
case CHAIN_SHIRT:
case SCALE_MAIL:
case BREASTPLATE:
case HALF_PLATE:
return "medium";
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return "heavy";
default:
return "natural";
}
}
private Integer getDexCap(ArmorType armorType) {
switch (armorType) {
case HIDE:
case CHAIN_SHIRT:
case SCALE_MAIL:
case BREASTPLATE:
case HALF_PLATE:
return 2;
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return 0;
default:
return null;
}
}
private boolean isStealthDisadvantage(ArmorType armorType) {
switch (armorType) {
case PADDED:
case SCALE_MAIL:
case HALF_PLATE:
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return true;
default:
return false;
}
}
private boolean hasDetails(Monster monster) {
return (monster.age != null && !monster.age.isEmpty()) ||
(monster.height != null && !monster.height.isEmpty()) ||
(monster.weight != null && !monster.weight.isEmpty()) ||
(monster.eyes != null && !monster.eyes.isEmpty()) ||
(monster.skin != null && !monster.skin.isEmpty()) ||
(monster.hair != null && !monster.hair.isEmpty()) ||
(monster.appearance != null && !monster.appearance.isEmpty()) ||
(monster.backstory != null && !monster.backstory.isEmpty()) ||
(monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty());
}
private String buildDescription(Monster monster) {
StringBuilder sb = new StringBuilder();
if (monster.size != null && !monster.size.isEmpty()) sb.append(monster.size).append(" ");
if (monster.type != null && !monster.type.isEmpty()) sb.append(monster.type);
if (monster.subtype != null && !monster.subtype.isEmpty()) sb.append(" (").append(monster.subtype).append(")");
if (monster.alignment != null && !monster.alignment.isEmpty()) sb.append(", ").append(monster.alignment);
return sb.toString().trim();
}
private JsonArray buildSavingThrowsArray(Monster monster) {
JsonArray arr = new JsonArray();
checkSave(arr, "Strength", monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH), monster.getAbilityModifier(AbilityScore.STRENGTH) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH)));
checkSave(arr, "Dexterity", monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY), monster.getAbilityModifier(AbilityScore.DEXTERITY) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY)));
checkSave(arr, "Constitution", monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION), monster.getAbilityModifier(AbilityScore.CONSTITUTION) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION)));
checkSave(arr, "Intelligence", monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE), monster.getAbilityModifier(AbilityScore.INTELLIGENCE) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE)));
checkSave(arr, "Wisdom", monster.getSavingThrowProficiencyType(AbilityScore.WISDOM), monster.getAbilityModifier(AbilityScore.WISDOM) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.WISDOM)));
checkSave(arr, "Charisma", monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA), monster.getAbilityModifier(AbilityScore.CHARISMA) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA)));
return arr;
}
private void checkSave(JsonArray arr, String statName, ProficiencyType prof, int bonus) {
if (prof != null && !prof.equals(ProficiencyType.NONE)) {
arr.add(String.format(Locale.ROOT, "%s %+d", statName, bonus));
}
}
private JsonArray buildSkillsArray(Monster monster) {
JsonArray arr = new JsonArray();
if (monster.skills != null) {
for (Skill s : monster.skills) {
if (s != null && s.proficiencyType != null && !s.proficiencyType.equals(ProficiencyType.NONE)) {
int bonus = s.getSkillBonus(monster);
arr.add(String.format(Locale.ROOT, "%s %+d", s.name.toLowerCase(Locale.ROOT), bonus));
}
}
}
return arr;
}
private JsonArray toJsonArray(Collection<String> items) {
JsonArray arr = new JsonArray();
for (String s : items) {
if (s != null && !s.isEmpty()) arr.add(s);
}
return arr;
}
private JsonArray traitsToJsonArray(List<Trait> traits) {
JsonArray arr = new JsonArray();
for (Trait t : traits) {
if (t != null && t.name != null && !t.name.isEmpty()) {
JsonObject obj = new JsonObject();
obj.addProperty("name", t.name);
obj.addProperty("desc", t.description != null ? t.description : "");
arr.add(obj);
}
}
return arr;
}
}

View File

@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.EntityImporter;
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
import com.majinnaibu.monstercards.importers.Open5eImporter;
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
import com.majinnaibu.monstercards.models.Monster;
@@ -16,6 +17,7 @@ public class MonsterImportHelper {
static {
IMPORTERS.add(new MonsterJsonImporter());
IMPORTERS.add(new Open5eImporter());
IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new DnDBeyondImporter());
}

View File

@@ -0,0 +1,387 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Open5eImporter implements EntityImporter<Monster> {
private static final Pattern HIT_DICE_PATTERN = Pattern.compile("(\\d+)d(\\d+)");
@Override
public boolean canImport(@NonNull String input) {
String trimmed = input.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
return false;
}
try {
JsonElement element = JsonParser.parseString(trimmed);
if (!element.isJsonObject()) {
return false;
}
JsonObject root = element.getAsJsonObject();
if (root.has("ruleset_id") && root.get("ruleset_id").getAsString().equals("open5e")) {
return true;
}
if (root.has("properties") && root.getAsJsonObject("properties").has("abilities")) {
return true;
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
JsonObject root = JsonParser.parseString(input.trim()).getAsJsonObject();
Monster monster = new Monster();
if (root.has("uuid") && !root.get("uuid").isJsonNull()) {
try {
monster.id = UUID.fromString(root.get("uuid").getAsString());
} catch (Exception ignored) {
}
}
if (root.has("display_name") && !root.get("display_name").isJsonNull()) {
monster.name = root.get("display_name").getAsString();
}
JsonObject props = root.has("properties") ? root.getAsJsonObject("properties") : root;
if (monster.name == null || monster.name.isEmpty()) {
monster.name = getString(props, "name", "Unnamed Entity");
}
monster.size = getString(props, "size");
monster.type = getString(props, "type");
monster.subtype = getString(props, "subtype");
monster.alignment = getString(props, "alignment");
parseArmorClass(props, monster);
parseHitPoints(props, monster);
monster.walkSpeed = getInt(props, "speed", 30);
if (props.has("speed_desc")) {
String speedDesc = getString(props, "speed_desc");
if (!speedDesc.isEmpty()) {
monster.hasCustomSpeed = true;
monster.customSpeedDescription = speedDesc;
}
}
parseChallengeRating(props, monster);
parseAbilities(props, monster);
parseIdentityFields(props, monster);
parseSavingThrows(props, monster);
parseSkills(props, monster);
parseSenses(props, monster);
parseLanguages(props, monster);
parseDamageAndConditions(props, monster);
parseTraitsAndActions(props, monster);
if (props.has("source_url")) {
monster.sourceUrl = getString(props, "source_url");
}
return monster;
}
private void parseArmorClass(JsonObject props, Monster monster) {
if (props.has("equipped_armor") && props.get("equipped_armor").isJsonObject()) {
JsonObject armorObj = props.getAsJsonObject("equipped_armor");
String armorId = getString(armorObj, "id");
ArmorType armorType = ArmorType.valueOfString(armorId);
if (armorType != ArmorType.NONE) {
monster.armorType = armorType;
}
}
if (props.has("equipped_shield") && props.get("equipped_shield").isJsonObject()) {
JsonObject shieldObj = props.getAsJsonObject("equipped_shield");
monster.shieldBonus = getInt(shieldObj, "ac_bonus", 2);
}
if (monster.armorType == ArmorType.NONE && props.has("armor_description")) {
String armorDesc = getString(props, "armor_description");
if (!armorDesc.isEmpty()) {
monster.otherArmorDescription = armorDesc;
}
}
}
private void parseHitPoints(JsonObject props, Monster monster) {
if (props.has("hit_points") && props.get("hit_points").isJsonObject()) {
JsonObject hpObj = props.getAsJsonObject("hit_points");
int maxHp = getInt(hpObj, "max", getInt(hpObj, "current", 10));
if (hpObj.has("hit_dice") && !hpObj.get("hit_dice").isJsonNull()) {
String hitDiceStr = hpObj.get("hit_dice").getAsString();
Matcher m = HIT_DICE_PATTERN.matcher(hitDiceStr);
if (m.find()) {
monster.hitDice = Integer.parseInt(m.group(1));
}
}
if (hpObj.has("formula") && !hpObj.get("formula").isJsonNull()) {
monster.hasCustomHP = true;
monster.customHPDescription = String.format(Locale.ROOT, "%d (%s)", maxHp, hpObj.get("formula").getAsString());
}
}
}
private void parseChallengeRating(JsonObject props, Monster monster) {
String crStr = "";
if (props.has("cr") && !props.get("cr").isJsonNull()) {
crStr = props.get("cr").getAsString();
} else if (props.has("challenge_rating") && !props.get("challenge_rating").isJsonNull()) {
crStr = props.get("challenge_rating").getAsString();
}
if (!crStr.isEmpty()) {
monster.challengeRating = ChallengeRating.valueOfString(crStr);
}
}
private void parseAbilities(JsonObject props, Monster monster) {
if (!props.has("abilities") || !props.get("abilities").isJsonObject()) {
return;
}
JsonObject abs = props.getAsJsonObject("abilities");
monster.strengthScore = extractAbilityScore(abs, "strength");
monster.dexterityScore = extractAbilityScore(abs, "dexterity");
monster.constitutionScore = extractAbilityScore(abs, "constitution");
monster.intelligenceScore = extractAbilityScore(abs, "intelligence");
monster.wisdomScore = extractAbilityScore(abs, "wisdom");
monster.charismaScore = extractAbilityScore(abs, "charisma");
}
private int extractAbilityScore(JsonObject abs, String key) {
if (!abs.has(key) || abs.get(key).isJsonNull()) {
return 10;
}
JsonElement el = abs.get(key);
if (el.isJsonPrimitive()) {
return el.getAsInt();
} else if (el.isJsonObject() && el.getAsJsonObject().has("value")) {
return el.getAsJsonObject().get("value").getAsInt();
}
return 10;
}
private void parseIdentityFields(JsonObject props, Monster monster) {
monster.playerName = getString(props, "player_name");
monster.background = getString(props, "background");
monster.personalityTraits = getString(props, "personality_traits");
monster.ideals = getString(props, "ideals");
monster.bonds = getString(props, "bonds");
monster.flaws = getString(props, "flaws");
if (props.has("character_details") && props.get("character_details").isJsonObject()) {
JsonObject details = props.getAsJsonObject("character_details");
monster.age = getString(details, "age");
monster.height = getString(details, "height");
monster.weight = getString(details, "weight");
monster.eyes = getString(details, "eyes");
monster.skin = getString(details, "skin");
monster.hair = getString(details, "hair");
monster.appearance = getString(details, "appearance");
monster.backstory = getString(details, "backstory");
monster.alliesAndOrganizations = getString(details, "allies_and_organizations");
}
}
private void parseSavingThrows(JsonObject props, Monster monster) {
if (!props.has("saving_throws")) return;
JsonElement savesEl = props.get("saving_throws");
if (savesEl.isJsonArray()) {
JsonArray arr = savesEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String saveStr = arr.get(i).getAsString().toLowerCase(Locale.ROOT);
applySaveProficiency(monster, saveStr);
}
}
}
private void applySaveProficiency(Monster monster, String saveStr) {
if (saveStr.contains("str")) monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("dex")) monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("con")) monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("int")) monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("wis")) monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("cha")) monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
}
private void parseSkills(JsonObject props, Monster monster) {
if (!props.has("skills")) return;
JsonElement skillsEl = props.get("skills");
if (skillsEl.isJsonArray()) {
JsonArray arr = skillsEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String skillStr = arr.get(i).getAsString();
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
if (!skillName.isEmpty()) {
AbilityScore stat = getAbilityForSkill(skillName);
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
}
}
}
}
private AbilityScore getAbilityForSkill(String skillName) {
String lower = skillName.toLowerCase(Locale.ROOT);
if (lower.contains("acro") || lower.contains("sleight") || lower.contains("stealth")) return AbilityScore.DEXTERITY;
if (lower.contains("arca") || lower.contains("hist") || lower.contains("inve") || lower.contains("natu") || lower.contains("reli")) return AbilityScore.INTELLIGENCE;
if (lower.contains("anim") || lower.contains("insi") || lower.contains("medi") || lower.contains("perc") || lower.contains("surv")) return AbilityScore.WISDOM;
if (lower.contains("dece") || lower.contains("inti") || lower.contains("perf") || lower.contains("pers")) return AbilityScore.CHARISMA;
return AbilityScore.STRENGTH;
}
private void parseSenses(JsonObject props, Monster monster) {
if (props.has("senses")) {
String sensesStr = getString(props, "senses");
if (!sensesStr.isEmpty()) {
String[] parts = sensesStr.split(",");
for (String p : parts) {
if (!p.trim().isEmpty()) {
monster.senses.add(p.trim());
}
}
}
}
}
private void parseLanguages(JsonObject props, Monster monster) {
if (props.has("languages")) {
JsonElement langEl = props.get("languages");
if (langEl.isJsonArray()) {
JsonArray arr = langEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String langName = arr.get(i).getAsString();
if (!langName.isEmpty()) {
monster.languages.add(new Language(langName, true));
}
}
} else if (langEl.isJsonPrimitive()) {
String langStr = langEl.getAsString();
for (String l : langStr.split(",")) {
if (!l.trim().isEmpty()) {
monster.languages.add(new Language(l.trim(), true));
}
}
}
}
}
private void parseDamageAndConditions(JsonObject props, Monster monster) {
parseStringSet(props, "damage_immunities", monster.damageImmunities);
parseStringSet(props, "damage_resistances", monster.damageResistances);
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
}
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
if (!props.has(key)) return;
JsonElement el = props.get(key);
if (el.isJsonArray()) {
JsonArray arr = el.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String val = arr.get(i).getAsString();
if (!val.isEmpty()) set.add(capitalize(val));
}
} else if (el.isJsonPrimitive()) {
for (String val : el.getAsString().split(",")) {
if (!val.trim().isEmpty()) set.add(capitalize(val.trim()));
}
}
}
private void parseTraitsAndActions(JsonObject props, Monster monster) {
parseTraitList(props, "traits", monster.abilities);
parseTraitList(props, "actions", monster.actions);
parseTraitList(props, "bonus_actions", monster.bonusActions);
parseTraitList(props, "reactions", monster.reactions);
parseTraitList(props, "legendary_actions", monster.legendaryActions);
parseTraitList(props, "lair_actions", monster.lairActions);
parseTraitList(props, "regional_effects", monster.regionalActions);
parseTraitList(props, "mythic_actions", monster.mythicActions);
if (props.has("legendary_desc")) monster.legendaryActionsDescription = getString(props, "legendary_desc");
if (props.has("lair_desc")) monster.lairActionsDescription = getString(props, "lair_desc");
if (props.has("lair_end_note")) monster.lairActionsEndNote = getString(props, "lair_end_note");
if (props.has("regional_desc")) monster.regionalActionsDescription = getString(props, "regional_desc");
if (props.has("regional_end_note")) monster.regionalActionsEndNote = getString(props, "regional_end_note");
if (props.has("mythic_desc")) monster.mythicActionsDescription = getString(props, "mythic_desc");
}
private void parseTraitList(JsonObject props, String key, List<Trait> list) {
if (!props.has(key) || !props.get(key).isJsonArray()) return;
JsonArray arr = props.getAsJsonArray(key);
for (int i = 0; i < arr.size(); i++) {
JsonElement el = arr.get(i);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
String name = getString(obj, "name");
String desc = getString(obj, "desc");
if (desc.isEmpty()) desc = getString(obj, "description");
if (!name.isEmpty()) {
list.add(new Trait(name, desc));
}
}
}
}
private static String getString(JsonObject obj, String key) {
return getString(obj, key, "");
}
private static String getString(JsonObject obj, String key, String defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
return obj.get(key).getAsString();
}
return defaultVal;
}
private static int getInt(JsonObject obj, String key, int defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
try {
return obj.get(key).getAsInt();
} catch (Exception ignored) {
}
}
return defaultVal;
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
String[] words = str.split("\\s+");
StringBuilder sb = new StringBuilder();
for (String w : words) {
if (!w.isEmpty()) {
if (sb.length() > 0) sb.append(" ");
sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1).toLowerCase(Locale.ROOT));
}
}
return sb.toString();
}
}

View File

@@ -233,6 +233,66 @@ public class Monster {
@ColumnInfo(name = "mythic_actions_description", defaultValue = "")
public String mythicActionsDescription;
@NonNull
@ColumnInfo(name = "player_name", defaultValue = "")
public String playerName;
@NonNull
@ColumnInfo(name = "background", defaultValue = "")
public String background;
@NonNull
@ColumnInfo(name = "personality_traits", defaultValue = "")
public String personalityTraits;
@NonNull
@ColumnInfo(name = "ideals", defaultValue = "")
public String ideals;
@NonNull
@ColumnInfo(name = "bonds", defaultValue = "")
public String bonds;
@NonNull
@ColumnInfo(name = "flaws", defaultValue = "")
public String flaws;
@NonNull
@ColumnInfo(name = "age", defaultValue = "")
public String age;
@NonNull
@ColumnInfo(name = "height", defaultValue = "")
public String height;
@NonNull
@ColumnInfo(name = "weight", defaultValue = "")
public String weight;
@NonNull
@ColumnInfo(name = "eyes", defaultValue = "")
public String eyes;
@NonNull
@ColumnInfo(name = "skin", defaultValue = "")
public String skin;
@NonNull
@ColumnInfo(name = "hair", defaultValue = "")
public String hair;
@NonNull
@ColumnInfo(name = "appearance", defaultValue = "")
public String appearance;
@NonNull
@ColumnInfo(name = "backstory", defaultValue = "")
public String backstory;
@NonNull
@ColumnInfo(name = "allies_and_organizations", defaultValue = "")
public String alliesAndOrganizations;
public Monster() {
id = UUID.randomUUID();
name = "";
@@ -278,6 +338,21 @@ public class Monster {
wisdomSavingThrowProficiency = ProficiencyType.NONE;
charismaSavingThrowAdvantage = AdvantageType.NONE;
charismaSavingThrowProficiency = ProficiencyType.NONE;
playerName = "";
background = "";
personalityTraits = "";
ideals = "";
bonds = "";
flaws = "";
age = "";
height = "";
weight = "";
eyes = "";
skin = "";
hair = "";
appearance = "";
backstory = "";
alliesAndOrganizations = "";
skills = new HashSet<>();

View File

@@ -28,6 +28,7 @@ import com.majinnaibu.monstercards.MainActivity;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.Open5eImporter;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentDirections;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
@@ -88,9 +89,12 @@ public class LibraryFragment extends MCFragment {
ClipData clip = clipboard.getPrimaryClip();
if (clip != null && clip.getItemCount() > 0) {
CharSequence clipText = clip.getItemAt(0).getText();
if (clipText != null && new DnDBeyondImporter().canImport(clipText.toString())) {
input.setText(clipText.toString().trim());
input.selectAll();
if (clipText != null) {
String clipStr = clipText.toString().trim();
if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) {
input.setText(clipStr);
input.selectAll();
}
}
}
}

View File

@@ -162,11 +162,11 @@
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
<string name="action_import_from_url">Import from URL...</string>
<string name="dialog_import_url_title">Import from D&amp;D Beyond</string>
<string name="dialog_import_url_message">Enter or paste a D&amp;D Beyond character URL or Character ID:</string>
<string name="dialog_import_url_hint">e.g. https://www.dndbeyond.com/characters/49074997/s7sLyX</string>
<string name="action_import_from_url">Import Entity / Character...</string>
<string name="dialog_import_url_title">Import Entity or 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_hint">e.g. D&amp;D Beyond URL or Open5e JSON</string>
<string name="dialog_import">Import</string>
<string name="toast_importing_url">Importing character from D&amp;D Beyond...</string>
<string name="failed_to_import_url">Failed to import character. Please check the URL or character privacy settings.</string>
<string name="toast_importing_url">Importing entity...</string>
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
</resources>