diff --git a/scripts/app.js b/scripts/app.js index 0899031..dd72686 100644 --- a/scripts/app.js +++ b/scripts/app.js @@ -49,6 +49,27 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) { }; } + _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; @@ -89,12 +110,40 @@ 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]; + } + } + } } } diff --git a/scripts/importer.js b/scripts/importer.js index 66397d0..cbc5cd4 100644 --- a/scripts/importer.js +++ b/scripts/importer.js @@ -83,43 +83,53 @@ export class DHImporter { data.system.type = match[2].toLowerCase(); } else if (line.match(/^Difficulty:/i)) { - // Can be "Difficulty: 14" or "Difficulty: 11 | Thresholds: ..." - const valueMatch = line.match(/Difficulty:\s*(\d+)/i); - if (valueMatch) data.system.difficulty = parseInt(valueMatch[1]); + // Can be "Difficulty: 14" or "Difficulty: 11 | Thresholds: ..." or "Difficulty: 12Thresholds: 5/9HP: 4Stress: 3" - // Check for inline stuff (Old Format) + // 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) if (line.includes("|")) { const parts = line.split("|").map(p => p.trim()); for (const part of parts) { - 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]; - } + // Fallback for pipes if regex somehow missed or overwrote (unlikely but safe) + // Actually the regex above is better. } } } else if (line.match(/^(Attack:|ATK:)/i)) { - // "Attack: +1" or "ATK: +2 | Claws: Melee | ..." + // "Attack: +1" or "ATK: +2 | Claws: Melee | ..." or "ATK: +2Claws: Melee1d6+2 phy" 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 (!line.includes("|") && lineIndex + 1 < lines.length) { + if ((!restOfLine || restOfLine.length === 0) && 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 (line.includes("|")) { - // Inline formatting - const parts = line.split("|").slice(1).join("|"); // remove ATK part - DHImporter._parseAttackDetails(parts, data); + } 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.match(/^Experience:/i)) { @@ -182,8 +192,9 @@ 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)); @@ -212,51 +223,53 @@ export class DHImporter { } static _parseAttackDetails(line, data) { - // 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]; - } - } + // 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(); } - // 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(); + 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) { + data.system.attack.range = rangeMatch[1].toLowerCase(); + rest = rest.substring(rangeMatch[0].length).trim(); } - // Part 1: Damage - if (parts.length > 1) { - const dmgStr = parts[1]; - const dmgMatch = dmgStr.match(/^(.+?)\s+(\w+)$/); - let formula = dmgStr; + + // 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; let type = "physical"; + if (dmgMatch) { formula = dmgMatch[1]; type = dmgMatch[2].toLowerCase(); - - // 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" + multiplier: "flat", dice: "d6", + flatMultiplier: 1 } }); } @@ -292,21 +305,92 @@ export class DHImporter { img: "icons/svg/item-bag.svg", system: { description: `

${buffer.typeHint}: ${buffer.description}

`, - actions: {} + actions: {}, + featureForm: isAction ? "action" : "passive" } }; if (isAction) { const actionId = foundry.utils.randomID(); - item.system.actions[actionId] = { + const action = { _id: actionId, type: "attack", name: buffer.name, actionType: "action", img: "icons/svg/item-bag.svg", systemPath: "actions", - chatDisplay: true + chatDisplay: true, + cost: [], + damage: { parts: [], includeBase: false, direct: false }, + range: "", + roll: { + type: "action", // Default to action, switch to attack if detected + 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) { + action.range = rangeMatch[1].toLowerCase(); + } + + // 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"; + + 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; @@ -379,7 +463,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)); @@ -472,12 +556,23 @@ export class DHImporter { data.items = finalItems; // Clean up temporary flags + const shouldOpen = data.openSheet; delete data.foundFeatures; delete data.useFeatures; + delete data.openSheet; - actorsToCreate.push(data); + actorsToCreate.push({ data, shouldOpen }); } - await Actor.createDocuments(actorsToCreate); + const 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 (actorsToCreate[i].shouldOpen) { + createdActors[i].sheet.render(true); + } + } + } } } diff --git a/styles/importer.css b/styles/importer.css index aad3c84..18c6afc 100644 --- a/styles/importer.css +++ b/styles/importer.css @@ -254,4 +254,71 @@ 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/preview.hbs b/templates/preview.hbs index 6bebe31..cc25c33 100644 --- a/templates/preview.hbs +++ b/templates/preview.hbs @@ -2,16 +2,34 @@
{{#each parsedData as |actor idx|}}
-

- {{actor.name}} - Tier {{actor.system.tier}} {{actor.system.type}} -

+
+
+ + +
+

+ {{actor.name}} + Tier {{actor.system.tier}} {{actor.system.type}} +

+
{{#if actor.system.resources.hitPoints.max}}HP: {{actor.system.resources.hitPoints.max}}{{/if}} {{#if actor.system.resources.stress.max}}Stress: {{actor.system.resources.stress.max}}{{/if}} Difficulty: {{actor.system.difficulty}} + {{#if actor.system.attack.name}} + {{#if actor.system.attack.name}} + +
+ + +
+ Attack: {{actor.system.attack.name}} +
+ {{/if}} + {{/if}}
@@ -20,7 +38,15 @@ {{#each actor.items as |item itemIdx|}} {{#if (eq item.type "feature")}}
  • - {{item.name}} +
    +
    + + +
    + {{item.name}} +
    {{#if (lookup ../foundFeatures item.name)}}
    @@ -40,6 +66,12 @@ {{/each}}
    + +
    + +
  • {{/each}}