diff --git a/docs/Import-Export.md b/docs/Import-Export.md new file mode 100644 index 0000000..a3f65b1 --- /dev/null +++ b/docs/Import-Export.md @@ -0,0 +1,164 @@ +# Import & Export Architecture & Plan + +This document outlines the modular architecture and step-by-step technical roadmap for handling monster stat block imports, exports, and sharing across multiple formats in the **Monster Cards** application. + +--- + +## 1. Modular Importer Architecture + +To keep import/export code clean and maintainable, all format parsers implement a shared interface: + +```java +public interface EntityImporter { + /** + * Determines whether the given input string or payload can be handled by this importer. + */ + boolean canImport(@NonNull String input); + + /** + * Parses the raw input string into internal domain memory objects (e.g., Monster). + */ + @NonNull + T parse(@NonNull String input) throws Exception; +} +``` + +The output of any `EntityImporter` is an in-memory domain model (such as `Monster`), which is then passed to `MonsterImportFragment` for UI preview and Room database persistence. + +--- + +## 2. Step-by-Step Implementation Roadmap + +``` + ┌────────────────----------------────────────────────────┐ + │ Step 0: Restrict File Extension Intent Filters │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 1: Refactor Import Code to Shared Interface │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 2: Update Tetra-cube Importer to Newest Format │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 3: Import from D&D Beyond Character URL │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 4: Export to Custom Internal Format (Open5e) │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 5: Import from Custom Internal Format (Open5e) │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 6: Generic Android Share Button Feature │ + └────────────────────────────────────────────────────────┘ + │ + v + ┌────────────────----------------────────────────────────┐ + │ Step 7: Specific Share Targets (NFC, Bluetooth, URL) │ + └────────────────────────────────────────────────────────┘ +``` + +--- + +### Step 0: Restrict File Extension Intent Filters + +Restrict the Android app so it reacts **only** to `.monster` and `.monster.txt` files instead of all generic `.txt` files. + +1. **`AndroidManifest.xml`**: + Add `android:pathPattern` and `android:pathSuffix` constraints (`.monster` and `.monster.txt`) to the `` for `MainActivity`. +2. **`MainActivity.java`**: + Implement runtime display name validation querying `OpenableColumns.DISPLAY_NAME` via `ContentResolver` to filter out non-monster files passed via `content://` URIs. + +--- + +### Step 1: Refactor Import Code into Importer Architecture + +Extract the current parsing logic out of `MonsterImportHelper.java` into a standalone importer class implementing `EntityImporter`. + +- **New Class**: `TetraCubeMonsterImporter implements EntityImporter` +- **Responsibilities**: + - `canImport(json)`: Validates that the JSON string contains Tetra-cube properties (e.g. `hitDice`, `armorName`, `strPoints`). + - `parse(json)`: Converts the raw JSON payload into an in-memory `Monster` object. + +--- + +### Step 2: Update Tetra-cube Importer to Newest Format + +Update `TetraCubeMonsterImporter` to support newer fields from the Tetra-cube generator source (`js/statblock-script.js`): + +- **New Fields to Parse**: + - `bonusActions`: Array of `{ name, desc }` objects $\rightarrow$ mapped to `monster.actions` or bonus action structure. + - `mythics` / `isMythic` / `mythicDescription`: Array of mythic action objects and intro text $\rightarrow$ mapped to `monster.legendaryActions`. + - `blind`: Parse boolean flag to append `"(blind beyond this radius)"` to `blindsight`. + - Section intro text: Parse `legendariesDescription`, `lairDescription`, `regionalDescription`. + +--- + +### Step 3: Import from D&D Beyond Character URL + +Allow users to import monsters or characters directly from a D&D Beyond URL. + +- **Target URL Structure**: + `https://www.dndbeyond.com/characters/49074997` (where `49074997` is the internal character ID). +- **Service Endpoints**: + D&D Beyond character pages load data from background JSON endpoints such as: + `https://character-service.dndbeyond.com/character/v2/character/49074997` +- **New Importer Class**: `DnDBeyondImporter implements EntityImporter` +- **Pipeline**: + 1. Extract character ID (`49074997`) from user-provided D&D Beyond URL. + 2. Perform an asynchronous HTTP request (via OkHttp/Retrofit) to fetch the character JSON payload. + 3. Map D&D Beyond JSON fields (stats, modifiers, AC, HP, speed, actions, traits, spells) to our internal `Monster` memory model. + 4. Pass the parsed `Monster` to `MonsterImportFragment` for review and persistence. + +--- + +### 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`). + +- **New Class**: `Open5eExporter` +- **Output Architecture**: + - **Universal Envelope (`entity.json`)**: `uuid`, `ruleset_id: "open5e"`, `entity_type: "character"`, `display_name`, `version`, `tags`, `properties`. + - **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`). + +--- + +### Step 5: Import from Custom Internal Format (Open5e Specification) + +Implement import capability for custom Open5e JSON files. + +- **New Class**: `Open5eImporter implements EntityImporter` +- **Pipeline**: + - `canImport(json)`: Validates `"ruleset_id": "open5e"` and `"entity_type": "character"`. + - `parse(json)`: Unpacks `properties` into internal `Monster` domain models. + +--- + +### Step 6: Generic Android Share Button Feature + +Implement a generic "Share" feature allowing users to export and share monsters or collections from detail screens. + +- **UI Action**: Add a "Share" item to action menus in `MonsterDetailFragment` and `CollectionDetailFragment`. +- **Android Intent**: Uses standard Android `ACTION_SEND` intent with `Intent.EXTRA_STREAM` or `Intent.EXTRA_TEXT` to pass exported files to other apps (e.g. Email, Drive, Messaging, Files). + +--- + +### Step 7: Specific Share Targets & Channels + +Extend the generic sharing feature with specialized, direct sharing channels (to be implemented as individual sub-steps): + +- **Sub-Step 7.1 (NFC Sharing)**: Share monster data directly between devices via NFC (NDEF records / Android Beam). +- **Sub-Step 7.2 (Bluetooth / Wi-Fi Direct)**: Share monster files directly between nearby Android devices via Bluetooth / Wi-Fi. +- **Sub-Step 7.3 (Web URL with Embedded Payload)**: Generate a shareable Web URL containing compressed/base64-encoded monster JSON data. diff --git a/docs/ImportFromMonster.md b/docs/ImportFromMonster.md new file mode 100644 index 0000000..d84207b --- /dev/null +++ b/docs/ImportFromMonster.md @@ -0,0 +1,243 @@ +# Import Process & Schema Documentation + +> **Platform Note**: This documentation describes the import process and internal database schema for the **Android Application** (`com.majinnaibu.monstercards`). + +--- + +## 1. Overview of the Import Process + +The Monster Cards Android application can import monster stat blocks formatted in JSON from `.monster` or `.monster.txt` files shared or opened from other applications (such as file managers, messaging apps, or browsers). + +``` + +-------------------------------------------------------+ + | External .monster / .monster.txt file (JSON Payload) | + +-------------------------------------------------------+ + | + v + +-------------------------------------------------------+ + | MainActivity (Intent Handling: ACTION_VIEW / SEND) | + | - Extracts raw JSON string from Content/File URI | + +-------------------------------------------------------+ + | + v + +-------------------------------------------------------+ + | MonsterImportFragment & MonsterImportViewModel | + | - MonsterImportHelper.fromJSON(json) parses JSON | + | - Maps JSON fields to internal Monster domain model | + | - Renders UI preview with calculated AC, HP, & Markdown| + +-------------------------------------------------------+ + | + v + +-------------------------------------------------------+ + | User clicks "Import" Menu Action | + | - Generates new UUID | + | - Persists Monster to Room Database (monsters table) | + | - Rebuilds FTS index (monsters_fts) | + +-------------------------------------------------------+ +``` + +--- + +## 2. Source Format (.monster / .monster.txt) & File Extension Behavior + +The `.monster` file format is generated by Tetra-cube's D&D 5e Statblock Generator (`statblock-script.js`). + +### File Saving Behavior (`SavedData.SaveToFile`) +```javascript +SaveToFile: () => saveAs(new Blob([JSON.stringify(mon)], { + type: "text/plain;charset=utf-8" +}), mon.name.toLowerCase() + ".monster") +``` + +- **Desktop Browsers**: Save the file with the specified `.monster` filename. +- **Android Mobile Browsers & DownloadManager**: Because the `Blob` uses MIME type `text/plain;charset=utf-8`, Android file handlers append a `.txt` extension, creating `.monster.txt` files. + +--- + +## 3. JSON Parsing & Field Mapping (`MonsterImportHelper.java`) + +`MonsterImportHelper.fromJSON(String json)` uses Gson's `JsonParser` to parse the root JSON object and populate a `Monster` instance. + +### Complete Field Mapping Reference + +| JSON Field Name | Target `Monster` Field | Type / Default | Description / Conversion Logic | +| :--- | :--- | :--- | :--- | +| `name` | `monster.name` | `String` | Monster name | +| `size` | `monster.size` | `String` | Size string (e.g., `"medium"`, `"large"`) | +| `type` | `monster.type` | `String` | Creature type (e.g., `"humanoid"`, `"dragon"`) | +| `tag` | `monster.subtype` | `String` | Subtype / tag (e.g., `"elf"`, `"shapechanger"`) | +| `alignment` | `monster.alignment` | `String` | Alignment string (e.g., `"neutral good"`) | +| `hitDice` | `monster.hitDice` | `int` (default 1) | Number of hit dice | +| `armorName` | `monster.armorType` | `ArmorType` | Converted via `ArmorTypeConverter.armorTypeFromStringValue()` | +| `shieldBonus` | `monster.shieldBonus` | `int` | Shield AC bonus (+2 if present) | +| `natArmorBonus` | `monster.naturalArmorBonus` | `int` | Additional natural armor AC bonus | +| `otherArmorDesc` | `monster.otherArmorDescription` | `String` | Custom armor description string | +| `speed` | `monster.walkSpeed` | `int` | Walking speed in feet | +| `burrowSpeed` | `monster.burrowSpeed` | `int` | Burrowing speed in feet | +| `climbSpeed` | `monster.climbSpeed` | `int` | Climbing speed in feet | +| `flySpeed` | `monster.flySpeed` | `int` | Flying speed in feet | +| `hover` | `monster.canHover` | `boolean` | Whether flying creature can hover | +| `swimSpeed` | `monster.swimSpeed` | `int` | Swimming speed in feet | +| `customHP` | `monster.hasCustomHP` | `boolean` | Flag for override custom HP string | +| `customSpeed` | `monster.hasCustomSpeed` | `boolean` | Flag for override custom speed string | +| `hpText` | `monster.customHPDescription` | `String` | Custom HP text description | +| `speedDesc` | `monster.customSpeedDescription` | `String` | Custom speed text description | +| `strPoints` | `monster.strengthScore` | `int` (default 10) | Strength ability score | +| `dexPoints` | `monster.dexterityScore` | `int` (default 10) | Dexterity ability score | +| `conPoints` | `monster.constitutionScore` | `int` (default 10) | Constitution ability score | +| `intPoints` | `monster.intelligenceScore` | `int` (default 10) | Intelligence ability score | +| `wisPoints` | `monster.wisdomScore` | `int` (default 10) | Wisdom ability score | +| `chaPoints` | `monster.charismaScore` | `int` (default 10) | Charisma ability score | +| `blindsight` | `monster.senses` | `Set` | Added as `"blindsight ft."` if `N > 0` | +| `darkvision` | `monster.senses` | `Set` | Added as `"darkvision ft."` if `N > 0` | +| `tremorsense` | `monster.senses` | `Set` | Added as `"tremorsense ft."` if `N > 0` | +| `truesight` | `monster.senses` | `Set` | Added as `"truesight ft."` if `N > 0` | +| `telepathy` | `monster.telepathyRange` | `int` | Telepathy range in feet | +| `cr` | `monster.challengeRating` | `ChallengeRating` | Converted via `ChallengeRatingConverter` | +| `customCr` | `monster.customChallengeRatingDescription` | `String` | Custom CR text | +| `customProf` | `monster.customProficiencyBonus` | `int` | Custom proficiency bonus override | +| `abilities` | `monster.abilities` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `actions` | `monster.actions` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `bonusActions` | `monster.actions` *(Future)* | `List` | Bonus actions array | +| `reactions` | `monster.reactions` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `legendaries` | `monster.legendaryActions` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `mythics` | `monster.legendaryActions` *(Future)* | `List` | Mythic actions array | +| `lairs` | `monster.lairActions` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `regionals` | `monster.regionalActions` | `List` | List of JSON objects `{ "name": "...", "desc": "..." }` | +| `sthrows` | Saving throw proficiencies | `ProficiencyType` | Array of objects `{ "name": "str"\|"dex"\|"con"\|"int"\|"wis"\|"cha" }`. Sets proficiency to `PROFICIENT`. | +| `skills` | `monster.skills` | `Set` | Array of `{ "name": "...", "stat": "...", "note": " (ex)" }`. `note == " (ex)"` sets `EXPERTISE`. | +| `damageTypes`, `specialdamage` | Immunities / Resistances / Vulnerabilities | `Set` | Array of `{ "name": "...", "type": "i"\|"r"\|"v" }`. Sorted into immunities (`"i"`), resistances (`"r"`), or vulnerabilities (`"v"`). | +| `conditions` | `monster.conditionImmunities` | `Set` | Array of `{ "name": "..." }` | +| `languages` | `monster.languages` | `Set` | Array of `{ "name": "...", "speaks": true\|false }` | +| `understandsBut` | `monster.understandsButDescription` | `String` | Language qualifier text | + +--- + +## 4. Preview & Database Persistence + +1. **ViewModel Computation (`MonsterImportViewModel.java`)**: + - Computes calculated stat block values: + - **AC**: Calculated based on `armorType`, dexterity modifier, shield bonus, natural armor bonus, or custom description. + - **HP**: Calculated as `floor(hitDice * ((dieSize + 1) / 2 + conModifier))` where `dieSize` is derived from size (e.g., Medium = d8, Large = d10). + - **Dynamic Placeholders**: Replaces placeholders in trait descriptions (e.g., `[STR ATK]`, `[WIS SAVE]`) with computed attack bonuses and spell save DCs based on ability scores and proficiency bonuses. + - **Markdown Rendering**: Trait and action descriptions are parsed through `CommonMarkHelper` to format inline HTML/Markdown for display. + +2. **Persistence (`MonsterImportFragment.java`)**: + - When the user taps the **Import** action menu button (`R.id.menu_action_import_monster`): + 1. Assigns a new `UUID` to `monster.id`. + 2. Calls `MonsterRepository.addMonster(monster)` which executes an `INSERT` into the Room database asynchronously on an IO thread via RxJava3. + 3. Shows a `Snackbar` confirming import success and navigates to the library/detail view for the imported monster. + +--- + +## 5. Internal Database Schema (`monsters` Table) + +The app uses **Room Database** (Database Name: `monsters`, Version: `5`). + +### Table: `monsters` + +| Column Name | SQLite Data Type | Default Value | Description / Room TypeConverter | +| :--- | :--- | :--- | :--- | +| `id` | `TEXT` | Primary Key | UUID string (`UUIDConverter`) | +| `name` | `TEXT` | `""` | Monster name | +| `size` | `TEXT` | `""` | Size category (e.g. `"medium"`) | +| `type` | `TEXT` | `""` | Creature type | +| `subtype` | `TEXT` | `""` | Creature tag / subtype | +| `alignment` | `TEXT` | `""` | Alignment | +| `strength_score` | `INTEGER` | `10` | Strength score | +| `strength_saving_throw_advantage` | `TEXT` | `"none"` | Advantage type (`"none"`, `"advantage"`, `"disadvantage"`) | +| `strength_saving_throw_proficiency` | `TEXT` | `"none"` | Proficiency type (`"none"`, `"proficient"`, `"expertise"`) | +| `dexterity_score` | `INTEGER` | `10` | Dexterity score | +| `dexterity_saving_throw_advantage` | `TEXT` | `"none"` | Dexterity saving throw advantage | +| `dexterity_saving_throw_proficiency` | `TEXT` | `"none"` | Dexterity saving throw proficiency | +| `constitution_score` | `INTEGER` | `10` | Constitution score | +| `constitution_saving_throw_advantage` | `TEXT` | `"none"` | Constitution saving throw advantage | +| `constitution_saving_throw_proficiency` | `TEXT` | `"none"` | Constitution saving throw proficiency | +| `intelligence_score` | `INTEGER` | `10` | Intelligence score | +| `intelligence_saving_throw_advantage` | `TEXT` | `"none"` | Intelligence saving throw advantage | +| `intelligence_saving_throw_proficiency` | `TEXT` | `"none"` | Intelligence saving throw proficiency | +| `wisdom_score` | `INTEGER` | `10` | Wisdom score | +| `wisdom_saving_throw_advantage` | `TEXT` | `"none"` | Wisdom saving throw advantage | +| `wisdom_saving_throw_proficiency` | `TEXT` | `"none"` | Wisdom saving throw proficiency | +| `charisma_score` | `INTEGER` | `10` | Charisma score | +| `charisma_saving_throw_advantage` | `TEXT` | `"none"` | Charisma saving throw advantage | +| `charisma_saving_throw_proficiency` | `TEXT` | `"none"` | Charisma saving throw proficiency | +| `armor_type` | `TEXT` | `"none"` | String representation of `ArmorType` (`ArmorTypeConverter`) | +| `shield_bonus` | `INTEGER` | `0` | Shield AC bonus | +| `natural_armor_bonus` | `INTEGER` | `0` | Natural armor bonus | +| `other_armor_description` | `TEXT` | `""` | Custom armor description | +| `hit_dice` | `INTEGER` | `1` | Hit dice count | +| `has_custom_hit_points` | `INTEGER` | `0` | Boolean flag (0 = false, 1 = true) | +| `custom_hit_points_description` | `TEXT` | `""` | Override HP string | +| `walk_speed` | `INTEGER` | `0` | Walk speed in ft | +| `burrow_speed` | `INTEGER` | `0` | Burrow speed in ft | +| `climb_speed` | `INTEGER` | `0` | Climb speed in ft | +| `fly_speed` | `INTEGER` | `0` | Fly speed in ft | +| `can_hover` | `INTEGER` | `0` | Boolean flag | +| `swim_speed` | `INTEGER` | `0` | Swim speed in ft | +| `has_custom_speed` | `INTEGER` | `0` | Boolean flag | +| `custom_speed_description` | `TEXT` | NULL | Override speed string | +| `challenge_rating` | `TEXT` | `"1"` | String representation of `ChallengeRating` (`ChallengeRatingConverter`) | +| `custom_challenge_rating_description` | `TEXT` | `""` | Custom CR text | +| `custom_proficiency_bonus` | `INTEGER` | `0` | Custom proficiency bonus override | +| `telepathy_range` | `INTEGER` | `0` | Telepathy range in ft | +| `understands_but_description` | `TEXT` | `""` | Language qualifier description | +| `senses` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) | +| `skills` | `TEXT` | `"[]"` | JSON array of Skill objects (`SetOfSkillConverter`) | +| `damage_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) | +| `damage_resistances` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) | +| `damage_vulnerabilities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) | +| `condition_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) | +| `languages` | `TEXT` | `"[]"` | JSON array of Language objects (`SetOfLanguageConverter`) | +| `abilities` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | +| `actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | +| `reactions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | +| `lair_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | +| `legendary_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | +| `regional_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) | + +### Virtual FTS Table: `monsters_fts` + +Used for full-text search indexing: +```sql +CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` +USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`); +``` + +--- + +## 6. Component Objects Schema (JSON Serialized in DB) + +### 1. `Trait` +Used in `abilities`, `actions`, `reactions`, `lair_actions`, `legendary_actions`, and `regional_actions`: +```json +{ + "name": "Keen Smell", + "description": "The monster has advantage on Wisdom (Perception) checks that rely on smell." +} +``` + +### 2. `Skill` +Used in `skills`: +```json +{ + "name": "Perception", + "abilityScore": "WISDOM", + "advantageType": "NONE", + "proficiencyType": "PROFICIENT" +} +``` +- **`abilityScore`**: `"STRENGTH"`, `"DEXTERITY"`, `"CONSTITUTION"`, `"INTELLIGENCE"`, `"WISDOM"`, `"CHARISMA"`. +- **`advantageType`**: `"NONE"`, `"ADVANTAGE"`, `"DISADVANTAGE"`. +- **`proficiencyType`**: `"NONE"`, `"PROFICIENT"`, `"EXPERTISE"`. + +### 3. `Language` +Used in `languages`: +```json +{ + "mName": "Common", + "mSpeaks": true +} +``` +- **`mName`**: Name of the language (e.g., `"Elvish"`). +- **`mSpeaks`**: `true` if the creature can speak it; `false` if it only understands it.