diff --git a/module.json b/module.json index 9caf802..2dba168 100644 --- a/module.json +++ b/module.json @@ -1,7 +1,7 @@ { "id": "dh-importer", "title": "Daggerheart Statblock Importer", - "version": "1.2.2", + "version": "1.0.2", "compatibility": { "minimum": "13", "verified": "13" @@ -34,5 +34,5 @@ "description": "Imports Adversaries and Environments from text blocks into the Daggerheart system.", "url": "https://github.com/cptn-cosmo/dh-importer", "manifest": "https://git.geeks.gay/cosmo/dh-importer/raw/branch/main/module.json", - "download": "https://git.geeks.gay/cosmo/dh-importer/releases/download/1.2.2/dh-importer.zip" + "download": "https://git.geeks.gay/cosmo/dh-importer/releases/download/1.0.2/dh-importer.zip" } \ No newline at end of file diff --git a/scripts/app.js b/scripts/app.js index 466ac48..0899031 100644 --- a/scripts/app.js +++ b/scripts/app.js @@ -8,7 +8,6 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) { this.step = "input"; this.parsedData = null; this.parsedDataType = null; - this.inputText = ""; } static DEFAULT_OPTIONS = { @@ -46,42 +45,15 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) { types: { adversary: "Adversary", environment: "Environment" - }, - inputText: this.inputText, - folders: game.folders.filter(f => f.type === "Actor").sort((a, b) => a.name.localeCompare(b.name)), - packs: game.packs.filter(p => p.documentName === "Actor" && !p.locked) + } }; } - _onRender(context, options) { - super._onRender(context, options); - - // Bind File Pickers - this.element.querySelectorAll("img[data-edit]").forEach(img => { - img.addEventListener("click", ev => { - const field = img.dataset.edit; - const fp = new FilePicker({ - type: "image", - current: img.getAttribute("src"), - callback: path => { - img.src = path; - const input = this.element.querySelector(`input[name='${field}']`); - if (input) input.value = path; - } - }); - fp.browse(); - }); - }); - } - static async onParse(event, target) { const form = this.element; const text = form.querySelector("textarea[name='text']").value; const type = form.querySelector("select[name='type']").value; - // Store input text - this.inputText = text; - if (!text) { ui.notifications.warn("Please enter text to import."); return; @@ -117,47 +89,16 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) { const formData = new foundry.applications.ux.FormDataExtended(this.element).object; for (const actor of this.parsedData) { - // Update Actor Image - if (formData[`img.${actor.name}`]) { - actor.img = formData[`img.${actor.name}`]; - actor.prototypeToken.texture.src = formData[`img.${actor.name}`]; - } - - // Update Attack Image - if (formData[`attackImg.${actor.name}`]) { - actor.system.attack.img = formData[`attackImg.${actor.name}`]; - } - - // Open Sheet Flag - if (formData[`openSheet.${actor.name}`]) { - actor.openSheet = true; - } - actor.useFeatures = {}; for (const item of actor.items) { const key = `useFeatures.${actor.name}.${item.name}`; if (formData[key]) { actor.useFeatures[item.name] = formData[key]; } - - // Update Item Image - const itemImgKey = `itemImg.${actor.name}.${item.name}`; - if (formData[itemImgKey]) { - item.img = formData[itemImgKey]; - // Also update embedded action image if present - if (item.system.actions) { - for (const actionId in item.system.actions) { - item.system.actions[actionId].img = formData[itemImgKey]; - } - } - } } } - const importTarget = formData.importTarget || ""; - const sortTier = formData.sortTier || false; - - await DHImporter.createDocuments(this.parsedData, this.parsedDataType, { target: importTarget, sortTier }); + await DHImporter.createDocuments(this.parsedData, this.parsedDataType); ui.notifications.info(`Successfully imported ${this.parsedData.length} documents.`); this.close(); } catch (e) { diff --git a/scripts/importer.js b/scripts/importer.js index 58b97b6..6ad5614 100644 --- a/scripts/importer.js +++ b/scripts/importer.js @@ -16,8 +16,6 @@ export class DHImporter { const data = { name: "Unknown Adversary", type: "adversary", - img: "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg", - prototypeToken: { texture: { src: "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg" } }, system: { tier: 1, type: "standard", @@ -83,53 +81,43 @@ export class DHImporter { data.system.type = match[2].toLowerCase(); } else if (line.match(/^Difficulty:/i)) { - // Can be "Difficulty: 14" or "Difficulty: 11 | Thresholds: ..." or "Difficulty: 12Thresholds: 5/9HP: 4Stress: 3" + // Can be "Difficulty: 14" or "Difficulty: 11 | Thresholds: ..." + const valueMatch = line.match(/Difficulty:\s*(\d+)/i); + if (valueMatch) data.system.difficulty = parseInt(valueMatch[1]); - // Robust Regex Extraction - const diffMatch = line.match(/Difficulty:\s*(\d+)/i); - if (diffMatch) data.system.difficulty = parseInt(diffMatch[1]); - - const thMatch = line.match(/Thresholds:\s*(\d+)\/(\d+)/i); - if (thMatch) { - data.system.damageThresholds.major = parseInt(thMatch[1]); - data.system.damageThresholds.severe = parseInt(thMatch[2]); - } - - const hpMatch = line.match(/HP:\s*(\d+)/i); - if (hpMatch) data.system.resources.hitPoints.max = parseInt(hpMatch[1]); - - const stressMatch = line.match(/Stress:\s*(\d+)/i); - if (stressMatch) data.system.resources.stress.max = parseInt(stressMatch[1]); - - // Check for inline stuff (Old Format with Pipes) + // Check for inline stuff (Old Format) if (line.includes("|")) { const parts = line.split("|").map(p => p.trim()); for (const part of parts) { - // Fallback for pipes if regex somehow missed or overwrote (unlikely but safe) - // Actually the regex above is better. + if (part.startsWith("HP:")) { + data.system.resources.hitPoints.max = parseInt(part.split(":")[1]); + } else if (part.startsWith("Stress:")) { + data.system.resources.stress.max = parseInt(part.split(":")[1]); + } else if (part.startsWith("Thresholds:")) { + const th = part.split(":")[1].split("/").map(t => parseInt(t.trim())); + if (th[0]) data.system.damageThresholds.major = th[0]; + if (th[1]) data.system.damageThresholds.severe = th[1]; + } } } } else if (line.match(/^(Attack:|ATK:)/i)) { - // "Attack: +1" or "ATK: +2 | Claws: Melee | ..." or "ATK: +2Claws: Melee1d6+2 phy" + // "Attack: +1" or "ATK: +2 | Claws: Melee | ..." const bonusMatch = line.match(/(?:Attack|ATK):\s*([+-]?\d+)/i); if (bonusMatch) data.system.attack.roll.bonus = parseInt(bonusMatch[1]); - // Clean the line to remove "ATK: +2" part - let restOfLine = line.replace(/(?:Attack|ATK):\s*[+-]?\d+/, "").trim(); - // If split format e.g. "Attack: +1" and next line is "Claws: Melee..." - if ((!restOfLine || restOfLine.length === 0) && lineIndex + 1 < lines.length) { + if (!line.includes("|") && lineIndex + 1 < lines.length) { const nextLine = lines[lineIndex + 1]; if (nextLine.includes(":") && !nextLine.match(/^(Experience|Features|Motives|Difficulty)/)) { // Likely the weapon details: "Claws: Melee | 1d8+3 phy" DHImporter._parseAttackDetails(nextLine, data); lineIndex++; // Consume next line } - } else if (restOfLine.length > 0) { - // Inline formatting (smashed or piped) - if (restOfLine.startsWith("|")) restOfLine = restOfLine.substring(1).trim(); - DHImporter._parseAttackDetails(restOfLine, data); + } else if (line.includes("|")) { + // Inline formatting + const parts = line.split("|").slice(1).join("|"); // remove ATK part + DHImporter._parseAttackDetails(parts, data); } } else if (line.match(/^Experience:/i)) { @@ -192,9 +180,8 @@ export class DHImporter { // FEATURES else if (currentSection === "features") { + const featureMatch = line.match(/^(.+?)\s+[–-]\s+(\w+)(?::\s*(.*))?$/); // "Name - Type" (Description on next line) OR "Name - Type: Description" - // Supports hyphen (-), en-dash (–), em-dash (—) and optional surrounding spaces - const featureMatch = line.match(/^(.+?)\s*[–—-]\s*(\w+)(?::\s*(.*))?$/); if (featureMatch) { if (featureBuffer) data.items.push(DHImporter._createFeatureItem(featureBuffer)); @@ -223,87 +210,67 @@ export class DHImporter { } static _parseAttackDetails(line, data) { - // Claws: Melee | 1d8+3 phy OR Claws: Melee 1d8+3 phy OR Claws: Melee1d6+2 phy - - // 1. Extract Name - let name = "Attack"; - let rest = line; - - if (line.includes(":")) { - const parts = line.split(":"); - name = parts[0].trim(); - rest = parts.slice(1).join(":").trim(); + // Claws: Melee | 1d8+3 phy OR Claws: Melee 1d8+3 phy + // Simple heuristic splitting + let parts = line.split("|").map(p => p.trim()); + if (parts.length === 1) { + // Try splitting by : for name/range + const firstColon = line.indexOf(":"); + if (firstColon > -1) { + const name = line.substring(0, firstColon).trim(); + const rest = line.substring(firstColon + 1).trim(); + const rangeMatch = rest.match(/^(\w+)\s+(.*)$/); + if (rangeMatch) { + parts = [name + ":" + rangeMatch[1], rangeMatch[2]]; + } else { + parts = [line]; + } + } } - data.system.attack.name = name; - - // 2. Extract Range - // Ranges: Melee, Very Close, Close, Far, Very Far - const rangeMatch = rest.match(/^(Melee|Very Close|Close|Far|Very Far)/i); - if (rangeMatch) { - let rangeVal = rangeMatch[1].toLowerCase(); - if (rangeVal === "very close") rangeVal = "veryClose"; - if (rangeVal === "very far") rangeVal = "veryFar"; - data.system.attack.range = rangeVal; - rest = rest.substring(rangeMatch[0].length).trim(); + // Now parse parts roughly matching old logic + // Part 0: Name: Range + if (parts.length > 0) { + const nameRange = parts[0].split(":"); + data.system.attack.name = nameRange[0].trim(); + if (nameRange[1]) data.system.attack.range = nameRange[1].trim().toLowerCase(); } - - // 3. Extract Damage - // Formula + Type - if (rest) { - // Remove optional pipes - rest = rest.replace(/^\|\s*/, ""); - - // "1d6+2 phy" - const dmgMatch = rest.match(/^(.+?)\s+(\w+)$/); - let formula = rest; + // Part 1: Damage + if (parts.length > 1) { + const dmgStr = parts[1]; + const dmgMatch = dmgStr.match(/^(.+?)\s+(\w+)$/); + let formula = dmgStr; let type = "physical"; - if (dmgMatch) { formula = dmgMatch[1]; type = dmgMatch[2].toLowerCase(); - if (type === "phy") type = "physical"; - else if (type === "mag" || type === "magic") type = "magical"; - } + // Map short codes + if (type === "phy") type = "physical"; + else if (type === "mag") type = "magic"; + } data.system.attack.damage.parts.push({ type: [type], applyTo: "hitPoints", value: { custom: { enabled: true, formula: formula }, - multiplier: "flat", dice: "d6", - flatMultiplier: 1 + multiplier: "flat", dice: "d6" } }); } } static _parseExperienceLine(line, data) { - // Handle "Experience: Manipulate +2, Infiltrate +2" or "Knowledge (Arcana, History) +2" - const parts = line.split(","); - let buffer = ""; - - for (let part of parts) { - buffer = buffer ? buffer + "," + part : part; - - // Check if buffer matches "Name +Value" - // We trim to handle spaces around commas - const trimmed = buffer.trim(); - // Regex to match "Name +Value" at the end of the string - const match = trimmed.match(/(.+?)\s+([+-]?\d+)$/); - - if (match) { - const currentCount = Object.keys(data.system.experiences).length; - const key = "exp" + currentCount; - data.system.experiences[key] = { - name: match[1].trim(), - value: parseInt(match[2]) - }; - // Reset buffer as we found a complete experience - buffer = ""; - } + // "Ambusher +3" + const match = line.match(/(.+?)\s+([+-]?\d+)$/); + if (match) { + const currentCount = Object.keys(data.system.experiences).length; + const key = "exp" + currentCount; + data.system.experiences[key] = { + name: match[1].trim(), + value: parseInt(match[2]) + }; } - // If buffer remains (e.g. "Some Trait" without number), it is ignored, consistent with previous behavior. } /** @@ -315,115 +282,13 @@ export class DHImporter { // sometimes with specific actions embedded. // We will put the type in the description. - const isAction = buffer.typeHint?.toLowerCase() === "action"; - - const item = { + return { name: buffer.name, type: "feature", - img: "icons/svg/item-bag.svg", system: { - description: `
${buffer.typeHint}: ${buffer.description}
`, - actions: {}, - featureForm: isAction ? "action" : "passive" + description: `${buffer.typeHint}: ${buffer.description}
` } }; - - if (isAction) { - // Determine if this is an attack or a generic ability - const isAttack = buffer.description.match(/make.*?attack/i) || - buffer.description.match(/(?:deal|inflict|take)\s+(\d+(?:d\d+)?(?:[\s]*[\+\-][\s]*\d+)?)\s+(direct\s+)?(\w+)\s+damage/i); - - const actionType = isAttack ? "attack" : "ability"; - const rollType = isAttack ? "attack" : "ability"; - - const actionId = foundry.utils.randomID(); - - const action = { - _id: actionId, - type: actionType, - name: buffer.name, - actionType: "action", - img: "icons/svg/item-bag.svg", - systemPath: "actions", - chatDisplay: true, - cost: [], - damage: { parts: [], includeBase: false, direct: false }, - range: "", - roll: { - type: rollType, - diceRolling: { multiplier: "flat", dice: "d6" } - } - }; - - // Parse Fear Cost - if (buffer.description.match(/^spen[md]\s+(?:a|1)\s+fear/i)) { - action.cost.push({ - consumeOnSuccess: false, - scalable: false, - key: "fear", - value: 1, - itemId: null, - step: null - }); - } - - // Parse Stress Cost - if (buffer.description.match(/mark\s+(?:a|1)\s+stress/i)) { - action.cost.push({ - consumeOnSuccess: false, - scalable: false, - key: "stress", - value: 1, - itemId: null, - step: null - }); - } - - // Parse Attack Trigger - if (buffer.description.match(/make.*?attack/i)) { - action.roll.type = "attack"; - } - - // Parse Range - const rangeMatch = buffer.description.match(/(Melee|Very Close|Close|Far|Very Far)/i); - if (rangeMatch) { - let rangeVal = rangeMatch[1].toLowerCase(); - if (rangeVal === "very close") rangeVal = "veryClose"; - if (rangeVal === "very far") rangeVal = "veryFar"; - - action.range = rangeVal; - } - - // Parse Damage: "deal 3d4+10 direct physical damage" - // Matches: context? + formula + optional direct + type + "damage" - const damageMatch = buffer.description.match(/(?:deal|inflict|take)\s+(\d+(?:d\d+)?(?:[\s]*[\+\-][\s]*\d+)?)\s+(direct\s+)?(\w+)\s+damage/i); - - if (damageMatch) { - // remove spaces in formula for cleaner data - let formula = damageMatch[1].replace(/\s/g, ""); - const isDirect = !!damageMatch[2]; // Captured "direct " - let type = damageMatch[3].toLowerCase(); - - // Map short codes - if (type === "phy") type = "physical"; - else if (type === "mag" || type === "magic") type = "magical"; - - action.damage.direct = isDirect; - action.damage.parts.push({ - value: { - custom: { enabled: true, formula: formula }, - multiplier: "flat", dice: "d6", - flatMultiplier: 1 // Default - }, - applyTo: "hitPoints", - type: [type] - }); - } - - item.system.actions[actionId] = action; - } - - return item; } /** @@ -443,7 +308,6 @@ export class DHImporter { const data = { name: lines[0], type: "environment", - img: "systems/daggerheart/assets/icons/documents/actors/forest.svg", system: { tier: 1, type: "exploration", @@ -493,7 +357,7 @@ export class DHImporter { currentSection = "features"; } else if (currentSection === "features") { // Parsing Features: "Name - Type: Description" - const featureMatch = line.match(/^(.+?)\s*[–—-]\s*(\w+):\s*(.*)/); + const featureMatch = line.match(/^(.+?)\s+[–-]\s+(\w+):\s*(.*)/); if (featureMatch) { if (featureBuffer) { data.items.push(DHImporter._createFeatureItem(featureBuffer)); @@ -559,86 +423,19 @@ export class DHImporter { return results; } - static async createDocuments(dataList, type, options = {}) { + static async createDocuments(dataList, type) { if (!dataList || dataList.length === 0) return; - const { target, sortTier } = options; const Actor = getDocumentClass("Actor"); - const Folder = getDocumentClass("Folder"); const actorsToCreate = []; - // Determine Target Context - let targetFolderId = null; - let targetPack = null; - - if (target) { - if (game.packs.has(target)) { - targetPack = game.packs.get(target); - } else { - targetFolderId = target; - } - } - - // Folder Cache for Tier Sorting - const folderCache = {}; - - const getTierFolder = async (tier) => { - const folderName = `Tier ${tier}`; - const cacheKey = `${targetPack ? targetPack.collection : "world"}-${targetFolderId || "root"}-${tier}`; - - if (folderCache[cacheKey]) return folderCache[cacheKey]; - - // Parent ID (for creating subfolder) - const parentId = targetFolderId; - - // Search for existing folder - let folder; - if (targetPack) { - // Search in compendium index (requires V11+ folders) - // We'll rely on pack.folders if available (V11) - if (targetPack.folders) { - folder = targetPack.folders.find(f => f.name === folderName && f.folder?.id === parentId); - } - } else { - // Search in World - folder = game.folders.find(f => f.type === "Actor" && f.name === folderName && f.folder?.id === parentId); - } - - if (folder) { - folderCache[cacheKey] = folder.id; - return folder.id; - } - - // Create Folder if not found - try { - const folderData = { - name: folderName, - type: "Actor", - folder: parentId, - sorting: "a" - }; - - if (targetPack) { - folderData.pack = targetPack.collection; - } - - const newFolder = await Folder.create(folderData, { pack: targetPack?.collection }); - if (newFolder) { - folderCache[cacheKey] = newFolder.id; - return newFolder.id; - } - } catch (e) { - console.warn("Could not create Tier folder:", e); - // Fallback to parent - return parentId; - } - }; - for (const data of dataList) { - // Process Items + // Process Items based on 'useCompendium' flags or similar set by the UI + // The UI should have modified data.items or we loop through and replace based on user choice const finalItems = []; for (const item of data.items) { if (data.useFeatures && data.useFeatures[item.name]) { + // User selected to use existing feature const uuid = data.useFeatures[item.name]; const original = await fromUuid(uuid); if (original) { @@ -652,46 +449,13 @@ export class DHImporter { } data.items = finalItems; - // Handle Sorting / Folder Assignment - if (sortTier) { - const tier = data.system.tier !== undefined ? data.system.tier : 0; - data.folder = await getTierFolder(tier); - } else if (targetFolderId) { - data.folder = targetFolderId; - } - // Clean up temporary flags - const shouldOpen = data.openSheet; delete data.foundFeatures; delete data.useFeatures; - delete data.openSheet; - // If finding an existing actor to update could be a future feature, but for now we create new. - actorsToCreate.push({ data, shouldOpen }); + actorsToCreate.push(data); } - let createdActors = []; - if (targetPack) { - // Import to Compendium - // Note: createDocuments with pack option works in V10+ - const docs = actorsToCreate.map(a => a.data); - createdActors = await Actor.createDocuments(docs, { pack: targetPack.collection }); - } else { - // Import to World - createdActors = await Actor.createDocuments(actorsToCreate.map(a => a.data)); - } - - // Open sheets - if (createdActors && createdActors.length > 0) { - for (let i = 0; i < createdActors.length; i++) { - // If we imported to compendium, we generally don't open sheets immediately as they are in the pack - // But if the user really wants to, we'd have to retrieve the sheet from the pack. - // Standard behavior for compendium import: don't open. - - if (!targetPack && actorsToCreate[i].shouldOpen) { - createdActors[i].sheet.render(true); - } - } - } + await Actor.createDocuments(actorsToCreate); } } diff --git a/styles/importer.css b/styles/importer.css index 18c6afc..aad3c84 100644 --- a/styles/importer.css +++ b/styles/importer.css @@ -254,71 +254,4 @@ gap: 10px; padding-top: 10px; border-top: 1px solid #4a4a4a; -} - -/* --- New Styles for Image Pickers --- */ - -.dh-importer-preview .header-row { - display: flex; - align-items: center; - gap: 15px; - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 10px; -} - -.dh-importer-preview .header-row h3 { - margin: 0; - border: none; - padding: 0; - flex: 1; -} - -.dh-importer-preview .image-picker { - position: relative; - cursor: pointer; - border: 1px solid #7a6a4a; - border-radius: 4px; - overflow: hidden; - transition: box-shadow 0.2s; - flex-shrink: 0; -} - -.dh-importer-preview .image-picker:hover { - box-shadow: 0 0 5px #ffd700; -} - -.dh-importer-preview .image-picker img { - display: block; - object-fit: cover; - background: #000; -} - -.dh-importer-preview .attack-pill { - display: inline-flex; - align-items: center; - background: #2a2a2a; - padding: 4px 16px 4px 8px; - /* Adjusted padding for icon */ - border-radius: 4px; - border: 1px solid #444; - color: #ccc; - font-size: 1.1em; - box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); - gap: 5px; -} - -.dh-importer-preview .attack-pill .image-picker { - border: none; - box-shadow: none; - background: transparent; -} - -.dh-importer-preview .feature-header { - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; - margin-bottom: 5px; } \ No newline at end of file diff --git a/templates/importer.hbs b/templates/importer.hbs index 4ad3828..833799c 100644 --- a/templates/importer.hbs +++ b/templates/importer.hbs @@ -13,7 +13,7 @@