Updates monster imports from tetra cube.
This commit is contained in:
@@ -0,0 +1,663 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 6,
|
||||||
|
"identityHash": "0ab331e246de46304f847d7ed2d10124",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "monsters",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `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 '', PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtype",
|
||||||
|
"columnName": "subtype",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alignment",
|
||||||
|
"columnName": "alignment",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "strengthScore",
|
||||||
|
"columnName": "strength_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "strengthSavingThrowAdvantage",
|
||||||
|
"columnName": "strength_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "strengthSavingThrowProficiency",
|
||||||
|
"columnName": "strength_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dexterityScore",
|
||||||
|
"columnName": "dexterity_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dexteritySavingThrowAdvantage",
|
||||||
|
"columnName": "dexterity_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dexteritySavingThrowProficiency",
|
||||||
|
"columnName": "dexterity_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "constitutionScore",
|
||||||
|
"columnName": "constitution_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "constitutionSavingThrowAdvantage",
|
||||||
|
"columnName": "constitution_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "constitutionSavingThrowProficiency",
|
||||||
|
"columnName": "constitution_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "intelligenceScore",
|
||||||
|
"columnName": "intelligence_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "intelligenceSavingThrowAdvantage",
|
||||||
|
"columnName": "intelligence_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "intelligenceSavingThrowProficiency",
|
||||||
|
"columnName": "intelligence_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wisdomScore",
|
||||||
|
"columnName": "wisdom_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wisdomSavingThrowAdvantage",
|
||||||
|
"columnName": "wisdom_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wisdomSavingThrowProficiency",
|
||||||
|
"columnName": "wisdom_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charismaScore",
|
||||||
|
"columnName": "charisma_score",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charismaSavingThrowAdvantage",
|
||||||
|
"columnName": "charisma_saving_throw_advantage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charismaSavingThrowProficiency",
|
||||||
|
"columnName": "charisma_saving_throw_proficiency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "armorType",
|
||||||
|
"columnName": "armor_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'none'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shieldBonus",
|
||||||
|
"columnName": "shield_bonus",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "naturalArmorBonus",
|
||||||
|
"columnName": "natural_armor_bonus",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "otherArmorDescription",
|
||||||
|
"columnName": "other_armor_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hitDice",
|
||||||
|
"columnName": "hit_dice",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasCustomHP",
|
||||||
|
"columnName": "has_custom_hit_points",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "customHPDescription",
|
||||||
|
"columnName": "custom_hit_points_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "walkSpeed",
|
||||||
|
"columnName": "walk_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "burrowSpeed",
|
||||||
|
"columnName": "burrow_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "climbSpeed",
|
||||||
|
"columnName": "climb_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "flySpeed",
|
||||||
|
"columnName": "fly_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "canHover",
|
||||||
|
"columnName": "can_hover",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "swimSpeed",
|
||||||
|
"columnName": "swim_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasCustomSpeed",
|
||||||
|
"columnName": "has_custom_speed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "customSpeedDescription",
|
||||||
|
"columnName": "custom_speed_description",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "challengeRating",
|
||||||
|
"columnName": "challenge_rating",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'1'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "customChallengeRatingDescription",
|
||||||
|
"columnName": "custom_challenge_rating_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "customProficiencyBonus",
|
||||||
|
"columnName": "custom_proficiency_bonus",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "telepathyRange",
|
||||||
|
"columnName": "telepathy_range",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "understandsButDescription",
|
||||||
|
"columnName": "understands_but_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "senses",
|
||||||
|
"columnName": "senses",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "skills",
|
||||||
|
"columnName": "skills",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "damageImmunities",
|
||||||
|
"columnName": "damage_immunities",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "damageResistances",
|
||||||
|
"columnName": "damage_resistances",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "damageVulnerabilities",
|
||||||
|
"columnName": "damage_vulnerabilities",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "conditionImmunities",
|
||||||
|
"columnName": "condition_immunities",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "languages",
|
||||||
|
"columnName": "languages",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "abilities",
|
||||||
|
"columnName": "abilities",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "actions",
|
||||||
|
"columnName": "actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reactions",
|
||||||
|
"columnName": "reactions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lairActions",
|
||||||
|
"columnName": "lair_actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "legendaryActions",
|
||||||
|
"columnName": "legendary_actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "regionalActions",
|
||||||
|
"columnName": "regional_actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourceUrl",
|
||||||
|
"columnName": "source_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bonusActions",
|
||||||
|
"columnName": "bonus_actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mythicActions",
|
||||||
|
"columnName": "mythic_actions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "'[]'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "legendaryActionsDescription",
|
||||||
|
"columnName": "legendary_actions_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lairActionsDescription",
|
||||||
|
"columnName": "lair_actions_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lairActionsEndNote",
|
||||||
|
"columnName": "lair_actions_end_note",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "regionalActionsDescription",
|
||||||
|
"columnName": "regional_actions_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "regionalActionsEndNote",
|
||||||
|
"columnName": "regional_actions_end_note",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mythicActionsDescription",
|
||||||
|
"columnName": "mythic_actions_description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"defaultValue": "''"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "monsters_fts",
|
||||||
|
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtype",
|
||||||
|
"columnName": "subtype",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alignment",
|
||||||
|
"columnName": "alignment",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": []
|
||||||
|
},
|
||||||
|
"ftsVersion": "FTS4",
|
||||||
|
"ftsOptions": {
|
||||||
|
"tokenizer": "simple",
|
||||||
|
"tokenizerArgs": [],
|
||||||
|
"contentTable": "monsters",
|
||||||
|
"languageIdColumnName": "",
|
||||||
|
"matchInfo": "FTS4",
|
||||||
|
"notIndexedColumns": [],
|
||||||
|
"prefixSizes": [],
|
||||||
|
"preferredOrder": "ASC"
|
||||||
|
},
|
||||||
|
"contentSyncTriggers": [
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "collections",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "collection_monsters",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "collectionId",
|
||||||
|
"columnName": "collection_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "monsterId",
|
||||||
|
"columnName": "monster_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ordinal",
|
||||||
|
"columnName": "ordinal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_collection_monsters_collection_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"collection_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_collection_monsters_monster_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"monster_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "collections",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"collection_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "monsters",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"monster_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "dashboard_monsters",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "monsterId",
|
||||||
|
"columnName": "monster_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ordinal",
|
||||||
|
"columnName": "ordinal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_dashboard_monsters_monster_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"monster_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "monsters",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"monster_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ab331e246de46304f847d7ed2d10124')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import com.majinnaibu.monstercards.models.DashboardMonster;
|
|||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.MonsterFTS;
|
import com.majinnaibu.monstercards.models.MonsterFTS;
|
||||||
|
|
||||||
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 5)
|
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 6)
|
||||||
@TypeConverters({
|
@TypeConverters({
|
||||||
ArmorTypeConverter.class,
|
ArmorTypeConverter.class,
|
||||||
ChallengeRatingConverter.class,
|
ChallengeRatingConverter.class,
|
||||||
|
|||||||
@@ -52,6 +52,20 @@ public class MonsterCardsApplication extends Application {
|
|||||||
database.execSQL("INSERT INTO dashboard_monsters(monster_id, ordinal) SELECT id, 0 FROM monsters");
|
database.execSQL("INSERT INTO dashboard_monsters(monster_id, ordinal) SELECT id, 0 FROM monsters");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `source_url` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonus_actions` TEXT DEFAULT '[]'");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions` TEXT DEFAULT '[]'");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `legendary_actions_description` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_description` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_end_note` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_description` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_end_note` TEXT DEFAULT ''");
|
||||||
|
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions_description` TEXT DEFAULT ''");
|
||||||
|
}
|
||||||
|
};
|
||||||
private MonsterRepository m_monsterLibraryRepository;
|
private MonsterRepository m_monsterLibraryRepository;
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +89,7 @@ public class MonsterCardsApplication extends Application {
|
|||||||
.addMigrations(MIGRATION_2_3)
|
.addMigrations(MIGRATION_2_3)
|
||||||
.addMigrations(MIGRATION_3_4)
|
.addMigrations(MIGRATION_3_4)
|
||||||
.addMigrations(MIGRATION_4_5)
|
.addMigrations(MIGRATION_4_5)
|
||||||
|
.addMigrations(MIGRATION_5_6)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
// .fallbackToDestructiveMigration()
|
// .fallbackToDestructiveMigration()
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
JsonObject rootDict = element.getAsJsonObject();
|
JsonObject rootDict = element.getAsJsonObject();
|
||||||
return rootDict.has("hitDice") || rootDict.has("armorName") || rootDict.has("strPoints");
|
return rootDict.has("hitDice") || rootDict.has("armorName") || rootDict.has("strPoints")
|
||||||
|
|| rootDict.has("bonusActions") || rootDict.has("isLegendary") || rootDict.has("isMythic")
|
||||||
|
|| rootDict.has("legendariesDescription");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -85,10 +87,46 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|||||||
|
|
||||||
monster.abilities = getListOfTraits(rootDict, "abilities");
|
monster.abilities = getListOfTraits(rootDict, "abilities");
|
||||||
monster.actions = getListOfTraits(rootDict, "actions");
|
monster.actions = getListOfTraits(rootDict, "actions");
|
||||||
|
monster.bonusActions = getListOfTraits(rootDict, "bonusActions");
|
||||||
monster.reactions = getListOfTraits(rootDict, "reactions");
|
monster.reactions = getListOfTraits(rootDict, "reactions");
|
||||||
monster.legendaryActions = getListOfTraits(rootDict, "legendaries");
|
monster.legendaryActions = getListOfTraits(rootDict, "legendaries");
|
||||||
|
monster.mythicActions = getListOfTraits(rootDict, "mythics");
|
||||||
monster.lairActions = getListOfTraits(rootDict, "lairs");
|
monster.lairActions = getListOfTraits(rootDict, "lairs");
|
||||||
monster.regionalActions = getListOfTraits(rootDict, "regionals");
|
monster.regionalActions = getListOfTraits(rootDict, "regionals");
|
||||||
|
|
||||||
|
monster.legendaryActionsDescription = getString(rootDict, "legendariesDescription");
|
||||||
|
if (!monster.legendaryActionsDescription.isEmpty()) {
|
||||||
|
monster.legendaryActions.add(0, new Trait("Legendary Actions", monster.legendaryActionsDescription));
|
||||||
|
}
|
||||||
|
|
||||||
|
monster.mythicActionsDescription = getString(rootDict, "mythicDescription");
|
||||||
|
if (!monster.mythicActionsDescription.isEmpty() || !monster.mythicActions.isEmpty()) {
|
||||||
|
if (!monster.mythicActionsDescription.isEmpty()) {
|
||||||
|
monster.legendaryActions.add(new Trait("Mythic Actions", monster.mythicActionsDescription));
|
||||||
|
}
|
||||||
|
for (Trait mythic : monster.mythicActions) {
|
||||||
|
String traitName = mythic.name;
|
||||||
|
if (!traitName.toLowerCase(Locale.ROOT).startsWith("mythic action")) {
|
||||||
|
traitName = "Mythic Action: " + traitName;
|
||||||
|
}
|
||||||
|
monster.legendaryActions.add(new Trait(traitName, mythic.description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monster.lairActionsDescription = getString(rootDict, "lairDescription");
|
||||||
|
monster.lairActionsEndNote = getString(rootDict, "lairDescriptionEnd");
|
||||||
|
String fullLairDesc = combineDescriptions(monster.lairActionsDescription, monster.lairActionsEndNote);
|
||||||
|
if (!fullLairDesc.isEmpty()) {
|
||||||
|
monster.lairActions.add(0, new Trait("Lair Actions", fullLairDesc));
|
||||||
|
}
|
||||||
|
|
||||||
|
monster.regionalActionsDescription = getString(rootDict, "regionalDescription");
|
||||||
|
monster.regionalActionsEndNote = getString(rootDict, "regionalDescriptionEnd");
|
||||||
|
String fullRegionalDesc = combineDescriptions(monster.regionalActionsDescription, monster.regionalActionsEndNote);
|
||||||
|
if (!fullRegionalDesc.isEmpty()) {
|
||||||
|
monster.regionalActions.add(0, new Trait("Regional Effects", fullRegionalDesc));
|
||||||
|
}
|
||||||
|
|
||||||
addSavingThrows(monster, rootDict);
|
addSavingThrows(monster, rootDict);
|
||||||
monster.skills = getSetOfSkills(rootDict);
|
monster.skills = getSetOfSkills(rootDict);
|
||||||
monster.damageImmunities = getSetOfDamageTypes(rootDict, "damageTypes", "i");
|
monster.damageImmunities = getSetOfDamageTypes(rootDict, "damageTypes", "i");
|
||||||
@@ -104,6 +142,17 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|||||||
return monster;
|
return monster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String combineDescriptions(String desc1, String desc2) {
|
||||||
|
if (!desc1.isEmpty() && !desc2.isEmpty()) {
|
||||||
|
return desc1 + "\n\n" + desc2;
|
||||||
|
} else if (!desc1.isEmpty()) {
|
||||||
|
return desc1;
|
||||||
|
} else {
|
||||||
|
return desc2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String getString(JsonObject dict, String name) {
|
private static String getString(JsonObject dict, String name) {
|
||||||
return getString(dict, name, "");
|
return getString(dict, name, "");
|
||||||
}
|
}
|
||||||
@@ -169,7 +218,11 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|||||||
private static void addSense(Monster monster, JsonObject root, String name) {
|
private static void addSense(Monster monster, JsonObject root, String name) {
|
||||||
int distance = getInt(root, name);
|
int distance = getInt(root, name);
|
||||||
if (distance > 0) {
|
if (distance > 0) {
|
||||||
monster.senses.add(formatDistance(name, distance));
|
String senseStr = formatDistance(name, distance);
|
||||||
|
if ("blindsight".equals(name) && getBool(root, "blind")) {
|
||||||
|
senseStr += " (blind beyond this radius)";
|
||||||
|
}
|
||||||
|
monster.senses.add(senseStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,33 @@ public class Monster {
|
|||||||
@ColumnInfo(name = "regional_actions", defaultValue = "[]")
|
@ColumnInfo(name = "regional_actions", defaultValue = "[]")
|
||||||
public List<Trait> regionalActions;
|
public List<Trait> regionalActions;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "source_url", defaultValue = "")
|
||||||
|
public String sourceUrl;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "bonus_actions", defaultValue = "[]")
|
||||||
|
public List<Trait> bonusActions;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "mythic_actions", defaultValue = "[]")
|
||||||
|
public List<Trait> mythicActions;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "legendary_actions_description", defaultValue = "")
|
||||||
|
public String legendaryActionsDescription;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "lair_actions_description", defaultValue = "")
|
||||||
|
public String lairActionsDescription;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "lair_actions_end_note", defaultValue = "")
|
||||||
|
public String lairActionsEndNote;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "regional_actions_description", defaultValue = "")
|
||||||
|
public String regionalActionsDescription;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "regional_actions_end_note", defaultValue = "")
|
||||||
|
public String regionalActionsEndNote;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "mythic_actions_description", defaultValue = "")
|
||||||
|
public String mythicActionsDescription;
|
||||||
|
|
||||||
public Monster() {
|
public Monster() {
|
||||||
id = UUID.randomUUID();
|
id = UUID.randomUUID();
|
||||||
name = "";
|
name = "";
|
||||||
@@ -266,6 +293,15 @@ public class Monster {
|
|||||||
lairActions = new ArrayList<>();
|
lairActions = new ArrayList<>();
|
||||||
legendaryActions = new ArrayList<>();
|
legendaryActions = new ArrayList<>();
|
||||||
regionalActions = new ArrayList<>();
|
regionalActions = new ArrayList<>();
|
||||||
|
bonusActions = new ArrayList<>();
|
||||||
|
mythicActions = new ArrayList<>();
|
||||||
|
sourceUrl = "";
|
||||||
|
legendaryActionsDescription = "";
|
||||||
|
lairActionsDescription = "";
|
||||||
|
lairActionsEndNote = "";
|
||||||
|
regionalActionsDescription = "";
|
||||||
|
regionalActionsEndNote = "";
|
||||||
|
mythicActionsDescription = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMeta() {
|
public String getMeta() {
|
||||||
@@ -783,6 +819,22 @@ public class Monster {
|
|||||||
return actionDescriptions;
|
return actionDescriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getBonusActionDescriptions() {
|
||||||
|
ArrayList<String> actionDescriptions = new ArrayList<>();
|
||||||
|
for (Trait action : bonusActions) {
|
||||||
|
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
|
||||||
|
}
|
||||||
|
return actionDescriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getMythicActionDescriptions() {
|
||||||
|
ArrayList<String> actionDescriptions = new ArrayList<>();
|
||||||
|
for (Trait action : mythicActions) {
|
||||||
|
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
|
||||||
|
}
|
||||||
|
return actionDescriptions;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getReactionDescriptions() {
|
public List<String> getReactionDescriptions() {
|
||||||
ArrayList<String> actionDescriptions = new ArrayList<>();
|
ArrayList<String> actionDescriptions = new ArrayList<>();
|
||||||
for (Trait action : reactions) {
|
for (Trait action : reactions) {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class TetraCubeMonsterImporterTest {
|
|||||||
+ "\"cr\": \"1/4\""
|
+ "\"cr\": \"1/4\""
|
||||||
+ "}";
|
+ "}";
|
||||||
|
|
||||||
|
private static final String ANCIENT_BLACK_DRAGON_JSON = "{\"name\":\"Ancient Black Dragon\",\"size\":\"gargantuan\",\"type\":\"Dragon\",\"tag\":\"\",\"alignment\":\"chaotic evil\",\"hitDice\":\"21\",\"armorName\":\"natural armor\",\"shieldBonus\":0,\"natArmorBonus\":10,\"otherArmorDesc\":\"22 (natural armor)\",\"speed\":\"40\",\"burrowSpeed\":\"0\",\"climbSpeed\":\"0\",\"flySpeed\":\"80\",\"hover\":false,\"swimSpeed\":\"40\",\"customHP\":false,\"customSpeed\":false,\"hpText\":\"367 (21d20 + 147)\",\"speedDesc\":\"40 ft., fly 80 ft., swim 40 ft.\",\"strPoints\":\"27\",\"dexPoints\":\"14\",\"conPoints\":\"25\",\"intPoints\":\"16\",\"wisPoints\":\"15\",\"chaPoints\":\"19\",\"blindsight\":\"60\",\"blind\":false,\"darkvision\":\"120\",\"tremorsense\":\"0\",\"truesight\":\"0\",\"telepathy\":0,\"cr\":\"21\",\"customCr\":\"21 (33,000 XP)\",\"customProf\":7,\"isLegendary\":true,\"legendariesDescription\":\"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.\",\"isLair\":true,\"lairDescription\":\"When fighting inside its lair, the ancient black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the ancient black dragon can take one lair action to cause one of the following effects:\",\"lairDescriptionEnd\":\"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.\",\"isMythic\":true,\"mythicDescription\":\"If the ancient black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.\",\"isRegional\":true,\"regionalDescription\":\"The region containing the ancient black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:\",\"regionalDescriptionEnd\":\"If the ancient black dragon dies, the first two effects fade over the course of 3d10 days.\",\"properties\":[],\"abilities\":[{\"name\":\"Amphibious\",\"desc\":\"The dragon can breathe air and water.\"},{\"name\":\"Legendary Resistance (3/Day)\",\"desc\":\"If the dragon fails a saving throw, it can choose to succeed instead.\"}],\"actions\":[{\"name\":\"Multiattack\",\"desc\":\"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.\"},{\"name\":\"Bite\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 15 ft., one target. _Hit:_ 19 (2d10 + 8) piercing damage plus 9 (2d8) acid damage.\"},{\"name\":\"Claw\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 10 ft., one target. _Hit:_ 15 (2d6 + 8) slashing damage.\"},{\"name\":\"Tail\",\"desc\":\"_Melee Weapon Attack:_ +15 to hit, reach 20 ft., one target. _Hit:_ 17 (2d8 + 8) bludgeoning damage.\"},{\"name\":\"Frightful Presence\",\"desc\":\"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours.\"},{\"name\":\"Acid Breath (Recharge 5-6)\",\"desc\":\"The dragon exhales acid in a 90-foot line that is 10 feet wide. Each creature in that line must make a DC 22 Dexterity saving throw, taking 67 (15d8) acid damage on a failed save, or half as much damage on a successful one.\"}],\"bonusActions\":[],\"reactions\":[],\"legendaries\":[{\"name\":\"Detect\",\"desc\":\"The dragon makes a Wisdom (Perception) check.\"},{\"name\":\"Tail Attack\",\"desc\":\"The dragon makes a tail attack.\"},{\"name\":\"Wing Attack (Costs 2 Actions)\",\"desc\":\"The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 23 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.\"}],\"mythics\":[],\"lairs\":[],\"regionals\":[],\"sthrows\":[{\"name\":\"dex\",\"order\":1},{\"name\":\"con\",\"order\":2},{\"name\":\"wis\",\"order\":4},{\"name\":\"cha\",\"order\":5}],\"skills\":[{\"name\":\"perception\",\"stat\":\"wis\",\"note\":\" (ex)\"},{\"name\":\"stealth\",\"stat\":\"dex\"}],\"damagetypes\":[{\"name\":\"acid\",\"note\":\" (Immune)\",\"type\":\"i\"}],\"specialdamage\":[],\"conditions\":[],\"languages\":[{\"name\":\"Common\",\"speaks\":true},{\"name\":\"Draconic\",\"speaks\":true}],\"understandsBut\":\"\",\"shortName\":\"\",\"pluralName\":\"\",\"doubleColumns\":false,\"separationPoint\":5,\"damage\":[]}";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void canImport_validTetraCubeJson_returnsTrue() {
|
public void canImport_validTetraCubeJson_returnsTrue() {
|
||||||
assertTrue(importer.canImport(SAMPLE_TETRACUBE_JSON));
|
assertTrue(importer.canImport(SAMPLE_TETRACUBE_JSON));
|
||||||
@@ -68,6 +70,106 @@ public class TetraCubeMonsterImporterTest {
|
|||||||
assertEquals(14, monster.dexterityScore);
|
assertEquals(14, monster.dexterityScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_bonusActions_populatesBonusActions() throws Exception {
|
||||||
|
String json = "{"
|
||||||
|
+ "\"name\": \"Goblin Boss\","
|
||||||
|
+ "\"hitDice\": 4,"
|
||||||
|
+ "\"bonusActions\": [{\"name\": \"Nimble Escape\", \"desc\": \"Disengage or Hide as bonus action.\"}]"
|
||||||
|
+ "}";
|
||||||
|
Monster monster = importer.parse(json);
|
||||||
|
assertNotNull(monster);
|
||||||
|
assertEquals(1, monster.bonusActions.size());
|
||||||
|
assertEquals("Nimble Escape", monster.bonusActions.get(0).name);
|
||||||
|
assertEquals("Disengage or Hide as bonus action.", monster.bonusActions.get(0).description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_blindsightWithBlindFlag_appendsNote() throws Exception {
|
||||||
|
String json = "{"
|
||||||
|
+ "\"name\": \"Grimlock\","
|
||||||
|
+ "\"hitDice\": 3,"
|
||||||
|
+ "\"blindsight\": 30,"
|
||||||
|
+ "\"blind\": true"
|
||||||
|
+ "}";
|
||||||
|
Monster monster = importer.parse(json);
|
||||||
|
assertNotNull(monster);
|
||||||
|
assertTrue(monster.senses.contains("blindsight 30 ft. (blind beyond this radius)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_legendaryAndMythicDescriptions_parsedCorrectly() throws Exception {
|
||||||
|
String json = "{"
|
||||||
|
+ "\"name\": \"Ancient Dragon\","
|
||||||
|
+ "\"hitDice\": 20,"
|
||||||
|
+ "\"legendariesDescription\": \"The dragon can take 3 legendary actions.\","
|
||||||
|
+ "\"mythicDescription\": \"If mythic trait is active...\","
|
||||||
|
+ "\"mythics\": [{\"name\": \"Grasp\", \"desc\": \"Grabs a creature.\"}]"
|
||||||
|
+ "}";
|
||||||
|
Monster monster = importer.parse(json);
|
||||||
|
assertNotNull(monster);
|
||||||
|
assertEquals(3, monster.legendaryActions.size());
|
||||||
|
assertEquals("Legendary Actions", monster.legendaryActions.get(0).name);
|
||||||
|
assertEquals("Mythic Actions", monster.legendaryActions.get(1).name);
|
||||||
|
assertEquals("Mythic Action: Grasp", monster.legendaryActions.get(2).name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_lairAndRegionalDescriptions_parsedCorrectly() throws Exception {
|
||||||
|
String json = "{"
|
||||||
|
+ "\"name\": \"Lich\","
|
||||||
|
+ "\"hitDice\": 18,"
|
||||||
|
+ "\"lairDescription\": \"Lair intro.\","
|
||||||
|
+ "\"lairDescriptionEnd\": \"Lair end.\","
|
||||||
|
+ "\"regionalDescription\": \"Regional intro.\","
|
||||||
|
+ "\"regionalDescriptionEnd\": \"Regional end.\""
|
||||||
|
+ "}";
|
||||||
|
Monster monster = importer.parse(json);
|
||||||
|
assertNotNull(monster);
|
||||||
|
assertEquals(1, monster.lairActions.size());
|
||||||
|
assertEquals("Lair Actions", monster.lairActions.get(0).name);
|
||||||
|
assertEquals("Lair intro.\n\nLair end.", monster.lairActions.get(0).description);
|
||||||
|
assertEquals(1, monster.regionalActions.size());
|
||||||
|
assertEquals("Regional Effects", monster.regionalActions.get(0).name);
|
||||||
|
assertEquals("Regional intro.\n\nRegional end.", monster.regionalActions.get(0).description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_ancientBlackDragonExport_populatesAllSections() throws Exception {
|
||||||
|
Monster monster = importer.parse(ANCIENT_BLACK_DRAGON_JSON);
|
||||||
|
assertNotNull(monster);
|
||||||
|
assertEquals("Ancient Black Dragon", monster.name);
|
||||||
|
assertEquals("gargantuan", monster.size);
|
||||||
|
assertEquals("Dragon", monster.type);
|
||||||
|
assertEquals("chaotic evil", monster.alignment);
|
||||||
|
assertEquals(27, monster.strengthScore);
|
||||||
|
|
||||||
|
// Abilities
|
||||||
|
assertEquals(2, monster.abilities.size());
|
||||||
|
assertEquals("Amphibious", monster.abilities.get(0).name);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
assertEquals(6, monster.actions.size());
|
||||||
|
assertEquals("Multiattack", monster.actions.get(0).name);
|
||||||
|
|
||||||
|
// Legendary + Mythic Actions
|
||||||
|
// 1 legendary intro + 3 legendary actions + 1 mythic intro = 5 items
|
||||||
|
assertEquals(5, monster.legendaryActions.size());
|
||||||
|
assertEquals("Legendary Actions", monster.legendaryActions.get(0).name);
|
||||||
|
assertEquals("Detect", monster.legendaryActions.get(1).name);
|
||||||
|
assertEquals("Mythic Actions", monster.legendaryActions.get(4).name);
|
||||||
|
|
||||||
|
// Lair Actions
|
||||||
|
assertEquals(1, monster.lairActions.size());
|
||||||
|
assertEquals("Lair Actions", monster.lairActions.get(0).name);
|
||||||
|
assertTrue(monster.lairActions.get(0).description.contains("invoke the ambient magic"));
|
||||||
|
|
||||||
|
// Regional Effects
|
||||||
|
assertEquals(1, monster.regionalActions.size());
|
||||||
|
assertEquals("Regional Effects", monster.regionalActions.get(0).name);
|
||||||
|
assertTrue(monster.regionalActions.get(0).description.contains("warped by the creature's presence"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void monsterImportHelper_fromJSON_delegatesToImporter() {
|
public void monsterImportHelper_fromJSON_delegatesToImporter() {
|
||||||
Monster monster = MonsterImportHelper.fromJSON(SAMPLE_TETRACUBE_JSON);
|
Monster monster = MonsterImportHelper.fromJSON(SAMPLE_TETRACUBE_JSON);
|
||||||
|
|||||||
@@ -106,32 +106,92 @@ Update `TetraCubeMonsterImporter` to support newer fields from the Tetra-cube ge
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 3: Import from D&D Beyond Character URL
|
### Step 3: Import from D&D Beyond URL & Web Services
|
||||||
|
|
||||||
Allow users to import monsters or characters directly from a D&D Beyond URL.
|
Allow users to import characters, monsters, and future entity types directly from D&D Beyond URLs using a multi-channel ingestion architecture.
|
||||||
|
|
||||||
- **Target URL Structure**:
|
- **Primary Target Share Links**:
|
||||||
`https://www.dndbeyond.com/characters/49074997` (where `49074997` is the internal character ID).
|
- Site Share Link: `https://www.dndbeyond.com/characters/49074997/s7sLyX` (primary link copied when tapping "Share" on D&D Beyond; contains numeric character ID `49074997` and share token `s7sLyX`).
|
||||||
- **Service Endpoints**:
|
- Standard Web Link: `https://www.dndbeyond.com/characters/49074997` (HTML page that loads background JavaScript to fetch stats).
|
||||||
D&D Beyond character pages load data from background JSON endpoints such as:
|
- URL Extractor Regex: `https?://(?:www\.)?dndbeyond\.com/characters/(\d+)(?:/([a-zA-Z0-9]+))?`
|
||||||
`https://character-service.dndbeyond.com/character/v2/character/49074997`
|
|
||||||
- **New Importer Class**: `DnDBeyondImporter implements EntityImporter<Monster>`
|
- **URL Ingestion Channels**:
|
||||||
- **Pipeline**:
|
1. **Android Share Sheet (`ACTION_SEND`)**:
|
||||||
1. Extract character ID (`49074997`) from user-provided D&D Beyond URL.
|
- Register `intent-filter` for `ACTION_SEND` with `text/plain` in `AndroidManifest.xml`.
|
||||||
2. Perform an asynchronous HTTP request (via OkHttp/Retrofit) to fetch the character JSON payload.
|
- Tapping "Share" on D&D Beyond passes `https://www.dndbeyond.com/characters/49074997/s7sLyX` directly into Monster Cards.
|
||||||
3. Map D&D Beyond JSON fields (stats, modifiers, AC, HP, speed, actions, traits, spells) to our internal `Monster` memory model.
|
2. **Android Web Link Chooser (`ACTION_VIEW`)**:
|
||||||
4. Pass the parsed `Monster` to `MonsterImportFragment` for review and persistence.
|
- Register `intent-filter` for `ACTION_VIEW` in `AndroidManifest.xml` with scheme `https`, host `www.dndbeyond.com`, and path prefixes `/characters/`, `/monsters/`, `/spells/`, etc.
|
||||||
|
- Allows Monster Cards to appear in the Android "Open with..." link chooser dialog.
|
||||||
|
3. **In-App Menu & Clipboard Auto-Detection**:
|
||||||
|
- Add an "Import from URL..." menu item in `LibraryFragment` and `MonsterImportFragment`.
|
||||||
|
- Displays a URL input dialog with automatic clipboard URL detection (`ClipboardManager`).
|
||||||
|
|
||||||
|
- **Known Endpoints & Web Asset Structure**:
|
||||||
|
- **Main Character Service Endpoint**:
|
||||||
|
- `https://character-service.dndbeyond.com/character/v5/character/49074997?includeCustomItems=true`
|
||||||
|
- Returns JSON payload containing full character statistics, attributes, modifiers, classes, race, inventory, and spells.
|
||||||
|
- Full reference sample JSON payload extracted to: [`docs/dndbeyond-character-sample.json`](file:///Users/tom/Projects/Apps/MonsterCards/docs/dndbeyond-character-sample.json).
|
||||||
|
- **Auxiliary Service Endpoints**:
|
||||||
|
- Vehicles: `https://character-service.dndbeyond.com/character/v5/vehicles?characterId=49074997`
|
||||||
|
- Vehicle Components: `https://character-service.dndbeyond.com/character/v5/vehicle/components?characterId=49074997`
|
||||||
|
- Known Infusions: `https://character-service.dndbeyond.com/character/v5/known-infusions?characterId=49074997`
|
||||||
|
- Infusion Items: `https://character-service.dndbeyond.com/character/v5/infusion/items?characterId=49074997`
|
||||||
|
- **Image & Avatar Assets**:
|
||||||
|
- Default Builder Avatar: `https://www.dndbeyond.com/Content/Skins/Waterdeep/images/characters/default-avatar-builder.png`
|
||||||
|
- Race Portrait Avatar: `portraitAvatarUrl` (e.g., `https://www.dndbeyond.com/avatars/2489/881/636680412207671648.jpeg`)
|
||||||
|
- Item / Attunement Icons: Returned in `inventory[].definition.avatarUrl` (e.g. `https://www.dndbeyond.com/avatars/19/144/636382339478303209.jpeg`, `https://www.dndbeyond.com/avatars/9249/564/637203446409923453.jpeg`).
|
||||||
|
- **Analytics / Telemetry**:
|
||||||
|
- `https://global.ketchcdn.com/web/v2/log...` (returns 204 No Content; user consent log endpoint ignored by importer).
|
||||||
|
|
||||||
|
- **Architecture (`DnDBeyondImporter implements EntityImporter<Monster>`)**:
|
||||||
|
- **URL Extractor**: Extracts `{characterId}` (`49074997`) from shared URLs (`https://www.dndbeyond.com/characters/49074997/s7sLyX`), trimming optional share tokens.
|
||||||
|
- **HTTP Service Client**: Performs an asynchronous HTTP GET request (via OkHttp/Retrofit) to `character-service.dndbeyond.com/character/v5/character/{characterId}?includeCustomItems=true`.
|
||||||
|
- **Domain Mapper**: Maps D&D Beyond JSON fields (stats, modifiers, AC, HP, speed, proficiencies, actions, bonus actions, reactions, spells, traits, avatar URLs) into the internal `Monster` domain model (`sourceUrl = "https://www.dndbeyond.com/characters/49074997/s7sLyX"`).
|
||||||
|
- **UI Preview**: Passes the generated `Monster` object to `MonsterImportFragment` for review and persistence.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 4: Export to Custom Internal Format (Open5e Specification)
|
### Step 4: Export to Custom Internal Format (Open5e Specification)
|
||||||
|
|
||||||
Implement export capability to output monsters and collections using our two-tier Open5e JSON specification (`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md`).
|
Implement export capability to output monsters and collections using our two-tier Open5e JSON specification (detailed in [`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md)).
|
||||||
|
|
||||||
- **New Class**: `Open5eExporter`
|
- **New Class**: `Open5eExporter`
|
||||||
- **Output Architecture**:
|
|
||||||
- **Universal Envelope (`entity.json`)**: `uuid`, `ruleset_id: "open5e"`, `entity_type: "character"`, `display_name`, `version`, `tags`, `properties`.
|
#### Open5e Ruleset Architecture & Schema Specification
|
||||||
- **Character Payload (`character.json`)**: Export ability scores, combat vitals (AC, HP formula, speed), proficiency bonuses, actions, reactions, legendary actions, features, and equipped items (`weapon.json`, `armor.json`, `shield.json`, `spell.json`).
|
The Open5e ruleset is part of a modular, versioned ruleset framework located at [`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/). In the future, this ruleset will be embedded directly into the application assets and versioned as our canonical model reference.
|
||||||
|
|
||||||
|
1. **Ruleset & Entity Identifiers**:
|
||||||
|
- Every ruleset defines a short string ID (`ruleset_id: "open5e"`) and a unique UUID (`uuid: "7a35e4d2-f67b-4890-a292-6a7593c72b21"`), declared in the ruleset manifest [`manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/manifest.json).
|
||||||
|
- Exported entities contain top-level identifiers (`uuid`, `ruleset_id`, `entity_type`, `display_name`) wrapped in a universal envelope.
|
||||||
|
- When importing a JSON payload like [`examples/character/goblin.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/examples/character/goblin.json), importers inspect `ruleset_id == "open5e"` to match the registered ruleset parser.
|
||||||
|
|
||||||
|
2. **Base Schemas ([`/Users/tom/Projects/TTRPG/CharacterDataFiles/schema/*.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/))**:
|
||||||
|
- **[`schema/entity.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/entity.json)**: The universal envelope schema wrapping all exported entities (`uuid`, `ruleset_id`, `entity_type`, `display_name`, `description`, `version`, `tags`, `template_id`, `properties`).
|
||||||
|
- **[`schema/manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/manifest.json)**: Schema defining ruleset manifest metadata, registered entity types, and sheet templates.
|
||||||
|
- **[`schema/entity-definition.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/schema/entity-definition.json)**: Meta-schema describing how entity types and property schemas are declared.
|
||||||
|
|
||||||
|
3. **Character Entity Schema ([`entities/character.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/entities/character.json))**:
|
||||||
|
- Defines the `properties` payload for Player Characters (PCs), NPCs, and Monsters/Creatures.
|
||||||
|
- Core fields in `properties`:
|
||||||
|
- **Identity & Vitals**: `size`, `type`, `subtype`, `alignment`, `armor_class`, `armor_description`, `hit_points` (`current`, `max`, `formula`, `hit_dice`), `speed`, `speed_desc`, `challenge_rating`, `cr`.
|
||||||
|
- **Ability Scores**: `abilities` (`strength`, `dexterity`, `constitution`, `intelligence`, `wisdom`, `charisma`).
|
||||||
|
- **Proficiencies & Senses**: `saving_throws`, `skills`, `senses`, `languages`.
|
||||||
|
- **Actions & Traits**: `traits`, `actions`, `reactions`, `legendary_actions`, `lair_actions`, `regional_effects`, and `spellcasting`.
|
||||||
|
|
||||||
|
4. **Rendering Templates (`template_id`)**:
|
||||||
|
- The `template_id` field in the universal envelope specifies how the entity should be rendered in external or host applications.
|
||||||
|
- Registered templates in [`manifest.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/manifest.json):
|
||||||
|
- `"stat_block"`: Monster Manual style stat block (`templates/stat_block.html`).
|
||||||
|
- `"character_card"`: Compact NPC summary card (`templates/character_card.html`).
|
||||||
|
- `"character_list_item"`: Fixed-height quick-reference list item (`templates/character_list_item.html`).
|
||||||
|
- `"character_sheet"` / `"character_sheet_alt"`: Full 5e interactive character sheets.
|
||||||
|
|
||||||
|
#### Exporter Output Requirements (`Open5eExporter`)
|
||||||
|
- **Envelope Generation**:
|
||||||
|
- Emits `$schema: "../../../schema/entity.json"`.
|
||||||
|
- Populates `uuid` (random UUID v4), `ruleset_id: "open5e"`, `entity_type: "character"`, `display_name: monster.name`, `template_id: "stat_block"`.
|
||||||
|
- **Payload Mapping**:
|
||||||
|
- Maps `Monster` fields to Open5e character properties in `properties` object conforming to [`entities/character.json`](file:///Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/entities/character.json).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,9 +229,30 @@ Extend the generic sharing feature with specialized, direct sharing channels (to
|
|||||||
|
|
||||||
- [x] **Step 0**: Restrict app intent filters in `AndroidManifest.xml` (`.monster` & `.monster.txt`) and add runtime filename validation in `MainActivity.java`.
|
- [x] **Step 0**: Restrict app intent filters in `AndroidManifest.xml` (`.monster` & `.monster.txt`) and add runtime filename validation in `MainActivity.java`.
|
||||||
- [x] **Step 1**: Refactor import & conversion code into a shared `EntityImporter<T>` interface and `TetraCubeMonsterImporter` class.
|
- [x] **Step 1**: Refactor import & conversion code into a shared `EntityImporter<T>` interface and `TetraCubeMonsterImporter` class.
|
||||||
- [ ] **Step 2**: Update Tetra-cube importer class to support the newest Tetra-cube format (`bonusActions`, `mythics`, `blind`, intro descriptions).
|
- [x] **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 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`).
|
- [ ] **Step 4**: Export to internal format described by Open5e document (`Open5eExporter`).
|
||||||
- [ ] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
|
- [ ] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
|
||||||
- [ ] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
|
- [ ] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
|
||||||
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).
|
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Schema Planning & Text Formatting Notes
|
||||||
|
|
||||||
|
### 4.1 Schema Architecture Considerations
|
||||||
|
- **Unified Action Entity / Type Column**:
|
||||||
|
- Instead of maintaining separate `List<Trait>` columns for each category (`actions`, `reactions`, `legendaryActions`, `lairActions`, `regionalActions`, etc.), a unified `monster_actions` table (or model list) with an `action_type` column (`ABILITY`, `ACTION`, `BONUS_ACTION`, `REACTION`, `LEGENDARY_ACTION`, `MYTHIC_ACTION`, `LAIR_ACTION`, `REGIONAL_EFFECT`) simplifies Room DB queries and enables a single reusable editor UI component.
|
||||||
|
- **Section Intro & End Note Metadata**:
|
||||||
|
- Store section-level intro text and end notes (e.g. `legendaryActionsDescription`, `lairActionsDescription`, `lairActionsEndNote`, `regionalActionsDescription`, `regionalActionsEndNote`, `mythicActionsDescription`) as dedicated metadata fields on the `Monster` entity rather than as artificial action traits.
|
||||||
|
|
||||||
|
### 4.2 Markdown & Content Formatting Support
|
||||||
|
- **Supported Markdown Syntax**:
|
||||||
|
- **Emphasis**: Italics (`_text_` or `*text*`) and Bold (`__text__` or `**text**`).
|
||||||
|
- **Lists**: Bulleted lists (`- `, `* `) and Numbered lists (`1. `, `2. `).
|
||||||
|
- **External Links**: Standard markdown links `[Text](https://...)`.
|
||||||
|
- **Internal Links (Future Deep Linking)**: Custom URI scheme `[Label](mc://<type>/<id>)` (e.g., `[Fireball](mc://spell/fireball)`) reserved for in-app navigation.
|
||||||
|
- **Data Model & Import/Export Handling**:
|
||||||
|
- All text fields across domain models, Room persistence, and import/export payloads store raw CommonMark markdown strings.
|
||||||
|
- Importers preserve raw markdown syntax as exported by source formats (Tetra-cube, Open5e, D&D Beyond).
|
||||||
|
- UI rendering layers handle conversion from CommonMark to Android Spanned text at display time.
|
||||||
|
|||||||
1
docs/ancient black dragon.monster
Normal file
1
docs/ancient black dragon.monster
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"Ancient Black Dragon","size":"gargantuan","type":"Dragon","tag":"","alignment":"chaotic evil","hitDice":"21","armorName":"natural armor","shieldBonus":0,"natArmorBonus":10,"otherArmorDesc":"22 (natural armor)","speed":"40","burrowSpeed":"0","climbSpeed":"0","flySpeed":"80","hover":false,"swimSpeed":"40","customHP":false,"customSpeed":false,"hpText":"367 (21d20 + 147)","speedDesc":"40 ft., fly 80 ft., swim 40 ft.","strPoints":"27","dexPoints":"14","conPoints":"25","intPoints":"16","wisPoints":"15","chaPoints":"19","blindsight":"60","blind":false,"darkvision":"120","tremorsense":"0","truesight":"0","telepathy":0,"cr":"21","customCr":"21 (33,000 XP)","customProf":7,"isLegendary":true,"legendariesDescription":"The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.","isLair":true,"lairDescription":"When fighting inside its lair, the ancient black dragon can invoke the ambient magic to take lair actions. On initiative count 20 (losing initiative ties), the ancient black dragon can take one lair action to cause one of the following effects:","lairDescriptionEnd":"The goblin can't repeat an effect until they have all been used, and it can't use the same effect two rounds in a row.","isMythic":true,"mythicDescription":"If the ancient black dragon's mythic trait is active, it can use the options below as legendary actions for 1 hour after using {Some Ability}.","isRegional":true,"regionalDescription":"The region containing the ancient black dragon's lair is warped by the creature's presence, which creates one or more of the following effects:","regionalDescriptionEnd":"If the ancient black dragon dies, the first two effects fade over the course of 3d10 days.","properties":[],"abilities":[{"name":"Amphibious","desc":"The dragon can breathe air and water."},{"name":"Legendary Resistance (3/Day)","desc":"If the dragon fails a saving throw, it can choose to succeed instead."}],"actions":[{"name":"Multiattack","desc":"The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."},{"name":"Bite","desc":"_Melee Weapon Attack:_ +15 to hit, reach 15 ft., one target. _Hit:_ 19 (2d10 + 8) piercing damage plus 9 (2d8) acid damage."},{"name":"Claw","desc":"_Melee Weapon Attack:_ +15 to hit, reach 10 ft., one target. _Hit:_ 15 (2d6 + 8) slashing damage."},{"name":"Tail","desc":"_Melee Weapon Attack:_ +15 to hit, reach 20 ft., one target. _Hit:_ 17 (2d8 + 8) bludgeoning damage."},{"name":"Frightful Presence","desc":"Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."},{"name":"Acid Breath (Recharge 5-6)","desc":"The dragon exhales acid in a 90-foot line that is 10 feet wide. Each creature in that line must make a DC 22 Dexterity saving throw, taking 67 (15d8) acid damage on a failed save, or half as much damage on a successful one."}],"bonusActions":[],"reactions":[],"legendaries":[{"name":"Detect","desc":"The dragon makes a Wisdom (Perception) check."},{"name":"Tail Attack","desc":"The dragon makes a tail attack."},{"name":"Wing Attack (Costs 2 Actions)","desc":"The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 23 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."}],"mythics":[],"lairs":[],"regionals":[],"sthrows":[{"name":"dex","order":1},{"name":"con","order":2},{"name":"wis","order":4},{"name":"cha","order":5}],"skills":[{"name":"perception","stat":"wis","note":" (ex)"},{"name":"stealth","stat":"dex"}],"damagetypes":[{"name":"acid","note":" (Immune)","type":"i"}],"specialdamage":[],"conditions":[],"languages":[{"name":"Common","speaks":true},{"name":"Draconic","speaks":true}],"understandsBut":"","shortName":"","pluralName":"","doubleColumns":false,"separationPoint":5,"damage":[]}
|
||||||
BIN
docs/ancient black dragon.png
Normal file
BIN
docs/ancient black dragon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
9858
docs/dndbeyond-character-sample.json
Normal file
9858
docs/dndbeyond-character-sample.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user