From 4e53a62a45220b2882a6f8f313f527ce672d3c2c Mon Sep 17 00:00:00 2001 From: valentine195 <38669521+valentine195@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:41:28 -0400 Subject: [PATCH] 1.1.0 - Added ability to import homebrew monsters from an improved initiative JSON file - Improved appearance of settings --- manifest.json | 2 +- package.json | 2 +- ...pFilesImporter.ts => DnDAppFilesImport.ts} | 135 +++++++------- src/importers/ImprovedInitiativeImport.ts | 173 ++++++++++++++++++ src/importers/index.ts | 2 + src/main.css | 8 + src/settings/settings.ts | 131 ++++++++----- 7 files changed, 336 insertions(+), 117 deletions(-) rename src/importers/{DnDAppFilesImporter.ts => DnDAppFilesImport.ts} (58%) create mode 100644 src/importers/ImprovedInitiativeImport.ts create mode 100644 src/importers/index.ts diff --git a/manifest.json b/manifest.json index 6aaec10a..d11f9460 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-5e-statblocks", "name": "5e Statblocks", - "version": "1.0.0", + "version": "1.1.0", "description": "Create 5e styled statblocks in Obsidian.md", "minAppVersion": "0.12.0", "author": "Jeremy Valentine", diff --git a/package.json b/package.json index 81754159..7dc8fb05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-5e-statblocks", - "version": "1.0.0", + "version": "1.1.0", "description": "Create 5e styled statblocks in Obsidian.md", "main": "main.js", "scripts": { diff --git a/src/importers/DnDAppFilesImporter.ts b/src/importers/DnDAppFilesImport.ts similarity index 58% rename from src/importers/DnDAppFilesImporter.ts rename to src/importers/DnDAppFilesImport.ts index d0d4e79f..e0d7c4b6 100644 --- a/src/importers/DnDAppFilesImporter.ts +++ b/src/importers/DnDAppFilesImport.ts @@ -6,76 +6,75 @@ import { StatBlockImporter } from "./StatBlockImporter"; */ import { Monster, Spell, Trait } from "@types"; import { titleCase } from "title-case"; -export class DnDAppFilesImporter { - public ImportEntitiesFromXml = async ( - ...files: File[] - ): Promise => { - return new Promise((resolve) => { - for (let xmlFile of files) { - const reader = new FileReader(); +export const ImportEntitiesFromXml = async ( + ...files: File[] +): Promise => { + return new Promise((resolve) => { + for (let xmlFile of files) { + const reader = new FileReader(); - reader.onload = async (event: any) => { - const xml: string = event.target.result; + reader.onload = async (event: any) => { + const xml: string = event.target.result; - const dom = new DOMParser().parseFromString( - xml, - "application/xml" - ); - const monsters = dom.getElementsByTagName("monster"); - const importedMonsters: Monster[] = []; - if (!monsters.length) return; - for (let monster of Array.from(monsters)) { - const importedMonster: Monster = { - name: getParameter(monster, "name"), - size: getSize(monster), - type: getParameter(monster, "type"), - subtype: getParameter(monster, "subtype"), - alignment: getParameter(monster, "alignment"), - ac: getAC(monster), - hp: Number(getHP(monster, "hp")), - hit_dice: getHP(monster, "hit_dice"), - speed: getParameter(monster, "speed"), - stats: [ - Number(getParameter(monster, "str")), - Number(getParameter(monster, "dex")), - Number(getParameter(monster, "con")), - Number(getParameter(monster, "int")), - Number(getParameter(monster, "wis")), - Number(getParameter(monster, "cha")) - ], - saves: getSaves(monster), - skillsaves: getSkillSaves(monster), - damage_vulnerabilities: getParameter( - monster, - "vulnerable" - ), - damage_resistances: getParameter(monster, "resist"), - damage_immunities: getParameter(monster, "immune"), - condition_immunities: getParameter( - monster, - "conditionImmune" - ), - senses: getParameter(monster, "senses"), - languages: getParameter(monster, "languages"), - cr: getParameter(monster, "cr"), - traits: getTraits(monster, "trait"), - spells: getSpells(monster), - actions: getTraits(monster, "action"), - legendary_actions: getTraits(monster, "legendary"), - reactions: getTraits(monster, "reaction"), - source: getSource(monster) - }; + const dom = new DOMParser().parseFromString( + xml, + "application/xml" + ); + const monsters = dom.getElementsByTagName("monster"); + const importedMonsters: Monster[] = []; + if (!monsters.length) return; + for (let monster of Array.from(monsters)) { + const importedMonster: Monster = { + name: getParameter(monster, "name"), + size: getSize(monster), + type: getParameter(monster, "type"), + subtype: getParameter(monster, "subtype"), + alignment: getParameter(monster, "alignment"), + ac: getAC(monster), + hp: Number(getHP(monster, "hp")), + hit_dice: getHP(monster, "hit_dice"), + speed: getParameter(monster, "speed"), + stats: [ + Number(getParameter(monster, "str")), + Number(getParameter(monster, "dex")), + Number(getParameter(monster, "con")), + Number(getParameter(monster, "int")), + Number(getParameter(monster, "wis")), + Number(getParameter(monster, "cha")) + ], + saves: getSaves(monster), + skillsaves: getSkillSaves(monster), + damage_vulnerabilities: getParameter( + monster, + "vulnerable" + ), + damage_resistances: getParameter(monster, "resist"), + damage_immunities: getParameter(monster, "immune"), + condition_immunities: getParameter( + monster, + "conditionImmune" + ), + senses: getParameter(monster, "senses"), + languages: getParameter(monster, "languages"), + cr: getParameter(monster, "cr"), + traits: getTraits(monster, "trait"), + spells: getSpells(monster), + actions: getTraits(monster, "action"), + legendary_actions: getTraits(monster, "legendary"), + reactions: getTraits(monster, "reaction"), + source: getSource(monster) + }; - importedMonsters.push(importedMonster); - } - resolve(importedMonsters); - }; + importedMonsters.push(importedMonster); + } + resolve(importedMonsters); + }; + + reader.readAsText(xmlFile); + } + }); +}; - reader.readAsText(xmlFile); - } - }); - }; -} function getParameter(monster: Element, tag: string): string { const element = monster.getElementsByTagName(tag); if (element && element.length) return element[0].textContent; @@ -221,9 +220,5 @@ function getSource(monster: Element): string { ); } } - console.log( - "🚀 ~ file: DnDAppFilesImporter.ts ~ line 226 ~ getSource ~ source", - source - ); return source; } diff --git a/src/importers/ImprovedInitiativeImport.ts b/src/importers/ImprovedInitiativeImport.ts new file mode 100644 index 00000000..13fa9238 --- /dev/null +++ b/src/importers/ImprovedInitiativeImport.ts @@ -0,0 +1,173 @@ +import { Monster } from "@types"; +const SAVES: Record< + string, + | "strength" + | "dexterity" + | "constitution" + | "intelligence" + | "wisdom" + | "charisma" +> = { + STR: "strength", + DEX: "dexterity", + CON: "constitution", + INT: "intelligence", + WIS: "wisdom", + CHA: "charisma" +}; +export const ImportEntitiesFromImprovedInitiative = async ( + ...files: File[] +): Promise> => { + return new Promise((resolve, reject) => { + for (let file of files) { + const reader = new FileReader(); + + reader.onload = async (event: any) => { + const importedMonsters: Map = new Map(); + try { + let json = JSON.parse(event.target.result); + const monsters = Object.keys(json).filter((key) => + /^Creatures/.test(key) + ); + for (let key of monsters) { + try { + const monster = json[key]; + const importedMonster: Monster = { + name: monster.Name, + source: monster.Source?.trim().length + ? monster.Source.trim() + : "Unknown - Improved Initiative File", + type: monster.Type.split(/,\s?/)[0].trim(), + subtype: "", + size: "", + alignment: monster.Type.split(/,\s?/)[1].trim(), + hp: monster.HP.Value, + hit_dice: monster.HP.Notes.replace( + /([()])/, + "" + ).trim(), + ac: monster.AC.Value, + speed: monster.Speed.join(", ").trim(), + stats: Object.values(monster.Abilities) as [ + number, + number, + number, + number, + number, + number + ], + damage_immunities: + monster.DamageImmunities?.join("; ") + .toLowerCase() + .trim() ?? "", + damage_resistances: + monster.DamageResistances?.join(", ") + .toLowerCase() + .trim() ?? "", + damage_vulnerabilities: + monster.DamageVulnerabilities?.join(", ") + .toLowerCase() + .trim() ?? "", + condition_immunities: + monster.ConditionImmunities?.join(", ") + .toLowerCase() + .trim() ?? "", + saves: + monster.Saves?.map( + ({ + Name, + Modifier + }: { + Name: string; + Modifier: number; + }) => { + return { + [SAVES[Name]]: Modifier + }; + } + ) ?? [], + skillsaves: + monster.Skills?.map( + ({ + Name, + Modifier + }: { + Name: string; + Modifier: number; + }) => { + return { + [Name]: Modifier + }; + } + ) ?? [], + senses: monster.Senses?.join(", ").trim() ?? "", + languages: + monster.Languages?.join(", ").trim() ?? "", + cr: monster.Challenge?.trim() ?? "", + traits: + monster.Traits?.map( + (trait: { + Name: string; + Content: string; + }) => { + return { + name: trait.Name, + desc: trait.Content + }; + } + ) ?? [], + actions: + monster.Actions?.map( + (trait: { + Name: string; + Content: string; + }) => { + return { + name: trait.Name, + desc: trait.Content + }; + } + ) ?? [], + reactions: + monster.Reactions?.map( + (trait: { + Name: string; + Content: string; + }) => { + return { + name: trait.Name, + desc: trait.Content + }; + } + ) ?? [], + legendary_actions: + monster.LegendaryActions?.map( + (trait: { + Name: string; + Content: string; + }) => { + return { + name: trait.Name, + desc: trait.Content + }; + } + ) ?? [] + }; + importedMonsters.set( + importedMonster.name, + importedMonster + ); + } catch (e) { + continue; + } + } + resolve(importedMonsters); + } catch (e) { + reject(); + } + }; + + reader.readAsText(file); + } + }); +}; diff --git a/src/importers/index.ts b/src/importers/index.ts new file mode 100644 index 00000000..ed07b517 --- /dev/null +++ b/src/importers/index.ts @@ -0,0 +1,2 @@ +export * from "./DnDAppFilesImport"; +export * from "./ImprovedInitiativeImport"; diff --git a/src/main.css b/src/main.css index b03ef90d..04803332 100644 --- a/src/main.css +++ b/src/main.css @@ -14,6 +14,14 @@ border-top: 1px solid var(--background-modifier-border); border-bottom: 0px solid var(--background-modifier-border); padding: 18px 0 0 0; + background-color: inherit; +} + +.statblock-monster-filter { + position: sticky; + top: -5px; + padding-top: 5px; + background-color: inherit; } .statblock-additional-container > .setting-item-heading:only-child { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 88c4568b..1f17a406 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -6,7 +6,10 @@ import { Setting, TextComponent } from "obsidian"; -import { DnDAppFilesImporter } from "src/importers/DnDAppFilesImporter"; +import { + ImportEntitiesFromXml, + ImportEntitiesFromImprovedInitiative +} from "src/importers"; import { MonsterSuggester } from "src/util/suggester"; export default class StatblockSettingTab extends PluginSettingTab { @@ -22,10 +25,22 @@ export default class StatblockSettingTab extends PluginSettingTab { containerEl.createEl("h2", { text: "5e Statblock Settings" }); - const importAppFile = new Setting(containerEl) + const importSettingsContainer = containerEl.createDiv( + "statblock-additional-container" + ); + + const importSetting = new Setting(importSettingsContainer) + .setName("Import Monsters") + .setDesc( + "Import monsters from monster files. Monsters are stored by name, so only the last monster by that name will be saved. This is destructive - any saved monster will be overwritten." + ); + + const importAdditional = + importSettingsContainer.createDiv("additional"); + const importAppFile = new Setting(importAdditional) .setName("Import DnDAppFile") .setDesc("Only import content that you own."); - const input = createEl("input", { + const inputAppFile = createEl("input", { attr: { type: "file", name: "dndappfile", @@ -33,24 +48,18 @@ export default class StatblockSettingTab extends PluginSettingTab { } }); - input.onchange = async () => { - const { files } = input; + inputAppFile.onchange = async () => { + const { files } = inputAppFile; if (!files.length) return; try { - const importer = new DnDAppFilesImporter(); - const importedMonsters = - await importer.ImportEntitiesFromXml( - ...Array.from(files) - ); + const importedMonsters = await ImportEntitiesFromXml( + ...Array.from(files) + ); try { await this.plugin.saveMonsters(importedMonsters); new Notice( `Successfully imported ${importedMonsters.length} monsters.` ); - console.log( - "🚀 ~ file: settings.ts ~ line 46 ~ StatblockSettingTab ~ input.onchange= ~ importedMonsters", - importedMonsters - ); } catch (e) { new Notice( `There was an issue importing the file${ @@ -63,22 +72,77 @@ export default class StatblockSettingTab extends PluginSettingTab { }; importAppFile.addButton((b) => { - b.setButtonText("Choose File").setTooltip("Import DnDAppFile"); + b.setButtonText("Choose File").setTooltip( + "Import DnDAppFile Data" + ); b.buttonEl.addClass("statblock-file-upload"); - b.buttonEl.appendChild(input); - b.onClick(() => input.click()); + b.buttonEl.appendChild(inputAppFile); + b.onClick(() => inputAppFile.click()); + }); + + const importImprovedInitiative = new Setting(importAdditional) + .setName("Import Improved Initiative Data") + .setDesc("Only import content that you own."); + const inputImprovedInitiative = createEl("input", { + attr: { + type: "file", + name: "dndappfile", + accept: ".json" + } + }); + + inputImprovedInitiative.onchange = async () => { + const { files } = inputImprovedInitiative; + if (!files.length) return; + try { + const importedMonsters = + await ImportEntitiesFromImprovedInitiative( + ...Array.from(files) + ); + + try { + await this.plugin.saveMonsters( + Array.from(importedMonsters.values()) + ); + new Notice( + `Successfully imported ${importedMonsters.size} monsters.` + ); + } catch (e) { + new Notice( + `There was an issue importing the file${ + files.length > 1 ? "s" : "" + }.` + ); + } + this.display(); + } catch (e) {} + }; + + importImprovedInitiative.addButton((b) => { + b.setButtonText("Choose File").setTooltip( + "Import Improved Initiative Data" + ); + b.buttonEl.addClass("statblock-file-upload"); + b.buttonEl.appendChild(inputImprovedInitiative); + b.onClick(() => inputImprovedInitiative.click()); }); const additionalContainer = containerEl.createDiv( - "statblock-additional-container" + "statblock-additional-container statblock-monsters" ); let monsterFilter: TextComponent; - const homebrewMonsters = new Setting(additionalContainer) + const filters = additionalContainer.createDiv( + "statblock-monster-filter" + ); + const searchMonsters = new Setting(filters) .setName("Homebrew Monsters") - .addText((t) => { + .addSearch((t) => { t.setPlaceholder("Filter Monsters"); monsterFilter = t; }); + /* const sourcesSetting = filters.createEl("details"); + sourcesSetting.createEl("summary", { text: "Filter Sources" }); */ + const additional = additionalContainer.createDiv("additional"); if (!this.plugin.data.size) { additional @@ -100,7 +164,7 @@ export default class StatblockSettingTab extends PluginSettingTab { this.plugin.sorted ); - homebrewMonsters.setDesc( + searchMonsters.setDesc( `Manage homebrew monsters. Currently: ${ suggester.getItems().length } monsters.` @@ -119,32 +183,9 @@ export default class StatblockSettingTab extends PluginSettingTab { this.display(); }; suggester.onInputChanged = () => - homebrewMonsters.setDesc( + searchMonsters.setDesc( `Manage homebrew monsters. Currently: ${suggester.filteredItems.length} monsters.` ); - - /* for (let monster of this.plugin.sorted) { - let setting = new Setting(additional) - .setName(monster) - .setDesc(this.plugin.data.get(monster).source ?? ""); - - setting.addExtraButton((b) => { - b.setIcon("trash") - .setTooltip("Delete") - .onClick(async () => { - try { - await this.plugin.deleteMonster(monster); - } catch (e) { - new Notice( - `There was an error deleting the monster:${ - `\n\n` + e.message - }` - ); - } - this.display(); - }); - }); - } */ } catch (e) { new Notice( "There was an error displaying the settings tab for 5e Statblocks."