Compare commits

..

No commits in common. "main" and "1.1.1" have entirely different histories.
main ... 1.1.1

5 changed files with 22 additions and 173 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "dh-importer", "id": "dh-importer",
"title": "Daggerheart Statblock Importer", "title": "Daggerheart Statblock Importer",
"version": "1.2.2", "version": "1.1.1",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "13"
@ -34,5 +34,5 @@
"description": "Imports Adversaries and Environments from text blocks into the Daggerheart system.", "description": "Imports Adversaries and Environments from text blocks into the Daggerheart system.",
"url": "https://github.com/cptn-cosmo/dh-importer", "url": "https://github.com/cptn-cosmo/dh-importer",
"manifest": "https://git.geeks.gay/cosmo/dh-importer/raw/branch/main/module.json", "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.1.1/dh-importer.zip"
} }

View file

@ -8,7 +8,6 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) {
this.step = "input"; this.step = "input";
this.parsedData = null; this.parsedData = null;
this.parsedDataType = null; this.parsedDataType = null;
this.inputText = "";
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -46,10 +45,7 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) {
types: { types: {
adversary: "Adversary", adversary: "Adversary",
environment: "Environment" 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)
}; };
} }
@ -79,9 +75,6 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) {
const text = form.querySelector("textarea[name='text']").value; const text = form.querySelector("textarea[name='text']").value;
const type = form.querySelector("select[name='type']").value; const type = form.querySelector("select[name='type']").value;
// Store input text
this.inputText = text;
if (!text) { if (!text) {
ui.notifications.warn("Please enter text to import."); ui.notifications.warn("Please enter text to import.");
return; return;
@ -154,10 +147,7 @@ export class DHImporterApp extends HandlebarsApplicationMixin(ApplicationV2) {
} }
} }
const importTarget = formData.importTarget || ""; await DHImporter.createDocuments(this.parsedData, this.parsedDataType);
const sortTier = formData.sortTier || false;
await DHImporter.createDocuments(this.parsedData, this.parsedDataType, { target: importTarget, sortTier });
ui.notifications.info(`Successfully imported ${this.parsedData.length} documents.`); ui.notifications.info(`Successfully imported ${this.parsedData.length} documents.`);
this.close(); this.close();
} catch (e) { } catch (e) {

View file

@ -279,19 +279,8 @@ export class DHImporter {
} }
static _parseExperienceLine(line, data) { static _parseExperienceLine(line, data) {
// Handle "Experience: Manipulate +2, Infiltrate +2" or "Knowledge (Arcana, History) +2" // "Ambusher +3"
const parts = line.split(","); const match = line.match(/(.+?)\s+([+-]?\d+)$/);
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) { if (match) {
const currentCount = Object.keys(data.system.experiences).length; const currentCount = Object.keys(data.system.experiences).length;
const key = "exp" + currentCount; const key = "exp" + currentCount;
@ -299,12 +288,8 @@ export class DHImporter {
name: match[1].trim(), name: match[1].trim(),
value: parseInt(match[2]) value: parseInt(match[2])
}; };
// Reset buffer as we found a complete experience
buffer = "";
} }
} }
// If buffer remains (e.g. "Some Trait" without number), it is ignored, consistent with previous behavior.
}
/** /**
* Helper to create feature item data * Helper to create feature item data
@ -329,18 +314,10 @@ export class DHImporter {
}; };
if (isAction) { 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 actionId = foundry.utils.randomID();
const action = { const action = {
_id: actionId, _id: actionId,
type: actionType, type: "attack",
name: buffer.name, name: buffer.name,
actionType: "action", actionType: "action",
img: "icons/svg/item-bag.svg", img: "icons/svg/item-bag.svg",
@ -350,7 +327,7 @@ export class DHImporter {
damage: { parts: [], includeBase: false, direct: false }, damage: { parts: [], includeBase: false, direct: false },
range: "", range: "",
roll: { roll: {
type: rollType, type: "attack", // Default to attack
diceRolling: { multiplier: "flat", dice: "d6" } diceRolling: { multiplier: "flat", dice: "d6" }
} }
}; };
@ -559,86 +536,19 @@ export class DHImporter {
return results; return results;
} }
static async createDocuments(dataList, type, options = {}) { static async createDocuments(dataList, type) {
if (!dataList || dataList.length === 0) return; if (!dataList || dataList.length === 0) return;
const { target, sortTier } = options;
const Actor = getDocumentClass("Actor"); const Actor = getDocumentClass("Actor");
const Folder = getDocumentClass("Folder");
const actorsToCreate = []; 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) { 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 = []; const finalItems = [];
for (const item of data.items) { for (const item of data.items) {
if (data.useFeatures && data.useFeatures[item.name]) { if (data.useFeatures && data.useFeatures[item.name]) {
// User selected to use existing feature
const uuid = data.useFeatures[item.name]; const uuid = data.useFeatures[item.name];
const original = await fromUuid(uuid); const original = await fromUuid(uuid);
if (original) { if (original) {
@ -652,43 +562,21 @@ export class DHImporter {
} }
data.items = finalItems; 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 // Clean up temporary flags
const shouldOpen = data.openSheet; const shouldOpen = data.openSheet;
delete data.foundFeatures; delete data.foundFeatures;
delete data.useFeatures; delete data.useFeatures;
delete data.openSheet; 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, shouldOpen });
} }
let createdActors = []; const createdActors = await Actor.createDocuments(actorsToCreate.map(a => a.data));
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 // Open sheets
if (createdActors && createdActors.length > 0) { if (createdActors && createdActors.length > 0) {
for (let i = 0; i < createdActors.length; i++) { 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 if (actorsToCreate[i].shouldOpen) {
// 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); createdActors[i].sheet.render(true);
} }
} }

View file

@ -13,7 +13,7 @@
<div class="form-group" style="flex: 1;"> <div class="form-group" style="flex: 1;">
<label>Paste Statblocks</label> <label>Paste Statblocks</label>
<textarea name="text" placeholder="Paste data here...">{{inputText}}</textarea> <textarea name="text" placeholder="Paste data here..."></textarea>
</div> </div>
<div class="action-footer"> <div class="action-footer">

View file

@ -76,35 +76,6 @@
{{/each}} {{/each}}
</div> </div>
<div class="import-options"
style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); border-radius: 5px;">
<div class="form-group">
<label>Import Target</label>
<select name="importTarget">
<option value="">World (Root)</option>
{{#if folders.length}}
<optgroup label="Folders">
{{#each folders}}
<option value="{{this.id}}">{{this.name}}</option>
{{/each}}
</optgroup>
{{/if}}
{{#if packs.length}}
<optgroup label="Compendiums">
{{#each packs}}
<option value="{{this.collection}}">{{this.metadata.label}}</option>
{{/each}}
</optgroup>
{{/if}}
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="sortTier"> Sort into Sub-folders by Tier
</label>
</div>
</div>
<div class="action-footer"> <div class="action-footer">
<button type="button" data-action="back"><i class="fas fa-arrow-left"></i> Back</button> <button type="button" data-action="back"><i class="fas fa-arrow-left"></i> Back</button>
<button type="button" data-action="import"><i class="fas fa-file-import"></i> Confirm Import</button> <button type="button" data-action="import"><i class="fas fa-file-import"></i> Confirm Import</button>