improved importer to detect attack features and actions
This commit is contained in:
parent
bbf4ecbc87
commit
74f123ecf2
4 changed files with 304 additions and 61 deletions
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<p><strong>${buffer.typeHint}</strong>: ${buffer.description}</p>`,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,3 +255,70 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -2,16 +2,34 @@
|
|||
<div class="actor-list">
|
||||
{{#each parsedData as |actor idx|}}
|
||||
<div class="actor-preview">
|
||||
<div class="header-row">
|
||||
<div class="image-picker">
|
||||
<img src="{{actor.img}}" data-edit="img.{{actor.name}}" height="48" width="48" />
|
||||
<input type="text" name="img.{{actor.name}}" value="{{actor.img}}" style="display:none">
|
||||
</div>
|
||||
<h3>
|
||||
<span>{{actor.name}}</span>
|
||||
<small>Tier {{actor.system.tier}} {{actor.system.type}}</small>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="stats">
|
||||
{{#if actor.system.resources.hitPoints.max}}<span><strong>HP:</strong>
|
||||
{{actor.system.resources.hitPoints.max}}</span>{{/if}}
|
||||
{{#if actor.system.resources.stress.max}}<span><strong>Stress:</strong>
|
||||
{{actor.system.resources.stress.max}}</span>{{/if}}
|
||||
<span><strong>Difficulty:</strong> {{actor.system.difficulty}}</span>
|
||||
{{#if actor.system.attack.name}}
|
||||
{{#if actor.system.attack.name}}
|
||||
<span class="attack-pill">
|
||||
<div class="image-picker small" style="border:none; margin-right:5px; width:24px; height:24px;">
|
||||
<img src="icons/svg/item-bag.svg" data-edit="attackImg.{{actor.name}}" height="24" width="24" />
|
||||
<input type="text" name="attackImg.{{actor.name}}" value="icons/svg/item-bag.svg"
|
||||
style="display:none">
|
||||
</div>
|
||||
<strong>Attack:</strong> {{actor.system.attack.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
|
|
@ -20,7 +38,15 @@
|
|||
{{#each actor.items as |item itemIdx|}}
|
||||
{{#if (eq item.type "feature")}}
|
||||
<li class="feature-item">
|
||||
<div class="feature-header">
|
||||
<div class="image-picker small">
|
||||
<img src="{{item.img}}" data-edit="itemImg.{{actor.name}}.{{item.name}}" height="24"
|
||||
width="24" />
|
||||
<input type="text" name="itemImg.{{actor.name}}.{{item.name}}" value="{{item.img}}"
|
||||
style="display:none">
|
||||
</div>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
|
||||
{{#if (lookup ../foundFeatures item.name)}}
|
||||
<div class="feature-match">
|
||||
|
|
@ -40,6 +66,12 @@
|
|||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="actor-options">
|
||||
<label>
|
||||
<input type="checkbox" name="openSheet.{{actor.name}}"> Open Sheet after Import
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue