Skip to content

Commit

Permalink
feature: NPC sheet export (#90)
Browse files Browse the repository at this point in the history
Added new mapping field for NPC and new mapping fo 5e
  • Loading branch information
gioppoluca authored Sep 17, 2023
1 parent 83cd74a commit dacc18b
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 3 deletions.
200 changes: 200 additions & 0 deletions mappings/dnd5eV11-npc.mapping
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* PDF: https://media.wizards.com/2016/dnd/downloads/5E_CharacterSheet_Fillable.pdf */
[
{ "pdf": "NpcName", "foundry": @name },
{ "pdf": "Race", "foundry": @system.details.race },
{ "pdf": "Type", "foundry": @system.details.type.value + " (" + game.dnd5e.config.actorSizes[@system.traits.size] + ")" + " - " + @system.details.alignment},
{ "pdf": "str", "foundry": @system.abilities.str.value },
{ "pdf": "STRmod", "foundry": @system.abilities.str.mod + " (sv: " + @system.abilities.str.save + ")"},
{ "pdf": "dex", "foundry": @system.abilities.dex.value },
{ "pdf": "DEXmod", "foundry": @system.abilities.dex.mod + " (sv: " + @system.abilities.dex.save + ")" },
{ "pdf": "con", "foundry": @system.abilities.con.value },
{ "pdf": "CONmod", "foundry": @system.abilities.con.mod + " (sv: " + @system.abilities.con.save + ")" },
{ "pdf": "int", "foundry": @system.abilities.int.value },
{ "pdf": "INTmod", "foundry": @system.abilities.int.mod + " (sv: " + @system.abilities.int.save + ")" },
{ "pdf": "wis", "foundry": @system.abilities.wis.value },
{ "pdf": "WISmod", "foundry": @system.abilities.wis.mod + " (sv: " + @system.abilities.wis.save + ")" },
{ "pdf": "cha", "foundry": @system.abilities.cha.value },
{ "pdf": "CHAmod", "foundry": @system.abilities.cha.mod + " (sv: " + @system.abilities.cha.save + ")" },
{ "pdf": "str-prof", "foundry": @system.abilities.str.proficient },
{ "pdf": "dex-prof", "foundry": @system.abilities.dex.proficient },
{ "pdf": "con-prof", "foundry": @system.abilities.con.proficient },
{ "pdf": "int-prof", "foundry": @system.abilities.int.proficient },
{ "pdf": "wis-prof", "foundry": @system.abilities.wis.proficient },
{ "pdf": "cha-prof", "foundry": @system.abilities.cha.proficient },
{ "pdf": "ProfBonus", "foundry": @system.attributes.prof },
{ "pdf": "AC", "foundry": @system.attributes.ac.value },
{ "pdf": "initiative", "foundry": @system.attributes.init.total },
{ "pdf": "HPMax", "foundry": @system.attributes.hp.max },
{ "pdf": "HPCurrent", "foundry": @system.attributes.hp.value },
{ "pdf": "Speed", "foundry": (function() {
const mo = actor.system.attributes.movement;
const mt = Object.entries(game.dnd5e.config.movementTypes).map(e => e[0]);
const ma = Object.entries(mo).filter(e => e[1] && mt.includes(e[0]));
if (mo.walk && ma?.length === 1) {
return `${ma[0][1]}${mo.units}${mo.hover ? "\n(hover)" : ""}`;
} else {
return ma.map(m => `${m[0].substring(0,2)}:${m[1]}${mo.units}`).join('\n').concat(mo.hover ? "\n(hover)" : "");
}
})()
},
{ "pdf": "features", "foundry": @items.filter(i => ["feat", "trait"].includes(i.type)).slice(0, 16).map(i => `${i.name} - ${i.system.source}: \n${((h) => {
const d = document.createElement("div");
d.innerHTML = h;
return d.textContent || d.innerText || "";
})(i.system.description.value.substring(0,599))}${(i.system.description.value.length>600)?'...':''}\n`).join("\n")
},
{ "pdf": "Wpn1 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[0]?.name || "" },
{ "pdf": "Wpn1 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[0];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn1 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[0]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn2 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[1]?.name || "" },
{ "pdf": "Wpn2 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[1];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn2 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[1]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn3 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[2]?.name || "" },
{ "pdf": "Wpn3 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[2];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn3 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[2]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn4 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[3]?.name || "" },
{ "pdf": "Wpn4 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[3];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn4 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[3]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn5 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[4]?.name || "" },
{ "pdf": "Wpn5 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[4];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn5 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[4]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn6 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[5]?.name || "" },
{ "pdf": "Wpn6 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[5];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn6 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[5]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Wpn7 Name", "foundry": @items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[6]?.name || "" },
{ "pdf": "Wpn7 AtkBonus", "foundry": (function() {
const theWeapon = actor.items.filter(i => i.type === 'weapon' && i.system.equipped && i.hasAttack && i.hasDamage)[6];
theWeapon?.prepareFinalAttributes();
return theWeapon?.labels?.toHit?.replace(/^\+ $/,"0") || ""
})()
},
{ "pdf": "Wpn7 Damage", "foundry": (function() {
const dda = Array.from(actor.itemTypes.weapon.filter(i => i.system.equipped && i.hasAttack && i.hasDamage))?.[6]?.labels.derivedDamage;
return !dda ? "" : dda.map(dd => `${dd.formula || ""} ${game.dnd5e.config.damageTypes[dd.damageType]}`).join('\n');
})()
},
{ "pdf": "Skills", "foundry": (function() {
let skill_text = "";
Object.keys(actor.system.skills).forEach(key => {
let row = game.dnd5e.config.skills[key].label + ": " + actor.system.skills[key].mod;
skill_text += row + "\n";
});
return skill_text;
})()
},
{ "pdf": "PersonalityTraits", "foundry": (function(h) {
const d = document.createElement("div");
d.innerHTML = h;
return d.textContent || d.innerText || "";
})(@system.details.trait)
},
{ "pdf": "Ideals", "foundry": (function(h) {
const d = document.createElement("div");
d.innerHTML = h;
return d.textContent || d.innerText || "";
})(@system.details.ideal)
},
{ "pdf": "Bonds", "foundry": (function(h) {
const d = document.createElement("div");
d.innerHTML = h;
return d.textContent || d.innerText || "";
})(@system.details.bond)
},
{ "pdf": "HD", "foundry": @system.attributes.hp.formula },
{ "pdf": "Flaws", "foundry": (function(h) {
const d = document.createElement("div");
d.innerHTML = h;
return d.textContent || d.innerText || "";
})(@system.details.flaw)
},
{ "pdf": "equipment", "foundry": @items.filter(i => ['weapon', 'equipment', 'tool'].includes(i.type)).map(i => (i.system.quantity <= 1) ? i.name : `${i.name} (${i.system.quantity})`).join(', ') },
{ "pdf": "CP", "foundry": @system.currency.cp || "" },
{ "pdf": "SP", "foundry": @system.currency.sp || "" },
{ "pdf": "EP", "foundry": @system.currency.ep || "" },
{ "pdf": "GP", "foundry": @system.currency.gp || "" },
{ "pdf": "PP", "foundry": @system.currency.pp || "" },
{ "pdf": "PPerception", "foundry": @system.skills.prc.passive },
{ "pdf": "biography", "foundry": (function(h) {
const d = document.createElement("div");
d.innerHTML = h.value;
return d.textContent || d.innerText || "";
})(@system.details.biography)
},
{ "pdf": "senses", "foundry": (function() {
let senses_text = "";
Object.keys(actor.system.attributes.senses).forEach(key => {
if(key != "units" && actor.system.attributes.senses[key] > 0){
let row = key + ": " + actor.system.attributes.senses[key] + " " + actor.system.attributes.senses["units"];
senses_text += row + "\n";
};
});
return senses_text;
})()
},
{ "pdf": "spells", "foundry": (function() {
let spell_text = "";
actor.items.filter(i => i.type === 'spell').sort((a,b)=>{return (a.system.level - b.system.level || a.name.localeCompare(b.name)) }).map(x => {x.name = x.name + ((typeof x.flags['items-with-spells-5e'] !== 'undefined') ? '[' +fromUuidSync(x.flags['items-with-spells-5e']['parent-item']).name + ']' : '');return x} ).forEach(object => {
let row = object.system.level + " - " + object.name;
spell_text += row + "\n";
});

return spell_text;
})()
},

{ "pdf": "", "foundry": "" }
]
Binary file added pdf/npc sheet 5e.pdf
Binary file not shown.
119 changes: 116 additions & 3 deletions scripts/pdfsheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ Hooks.on("init", () => {
default: "[]",
});

game.settings.register(Pdfconfig.ID, "mapping-npc", {
name: "pdfsheet.settings.mapping-npc.Name",
hint: "pdfsheet.settings.mapping-npc.Hint",
scope: "world",
config: true,
type: String,
default: "[]",
});

if (game.version && isNewerVersion(game.version, "9.230")) {
game.keybindings.register(Pdfconfig.ID, "showConfig", {
name: "Show Config",
Expand All @@ -35,10 +44,15 @@ Hooks.on("renderSettingsConfig", (app, html) => {
// Get the old text box
const oldTextBox = html[0].querySelector("[name='pdf-sheet.mapping']");

// NPC part (just duplicate the previous fields)
let editorNPC, newTextBoxNPC;

// Get the old text box
const oldTextBoxNPC = html[0].querySelector("[name='pdf-sheet.mapping-npc']");

// If Ace Library is enabled use an Ace Editor
if (game.modules.get("acelib")?.active) {
/* global ace */

// Create an editor
newTextBox = document.createElement("div");
editor = ace.edit(newTextBox);
Expand All @@ -60,12 +74,37 @@ Hooks.on("renderSettingsConfig", (app, html) => {
"changeAnnotation",
debounce(() => editor.getSession().setAnnotations(), 1)
);
newTextBoxNPC = document.createElement("div");
editorNPC = ace.edit(newTextBoxNPC);

// Set to the default options
editorNPC.setOptions(ace.userSettings);

// Set to JavaScript mode
editorNPC.session.setMode("ace/mode/javascript");

// Copy the value from the old textbox into the Ace Editor
editorNPC.setValue(oldTextBoxNPC.value);

// After a short wait (to make sure the editor is loaded), beautify the editor contents
setTimeout(() => editorNPC.execCommand("beautify"), 500);

// Hide annotations
editorNPC.getSession().on(
"changeAnnotation",
debounce(() => editorNPC.getSession().setAnnotations(), 1)
);
} else {
// Otherwise create new textarea
newTextBox = document.createElement("textarea");

// Copy the value from the old textbox into the new one
newTextBox.value = oldTextBox.value;
// Otherwise create new textarea NPC
newTextBoxNPC = document.createElement("textarea");

// Copy the value from the old textbox into the new one
newTextBoxNPC.value = oldTextBoxNPC.value;
}

// Don't show the old textbox
Expand All @@ -80,18 +119,38 @@ Hooks.on("renderSettingsConfig", (app, html) => {
// Insert the new textbox right after the old one
oldTextBox.after(newTextBox);

// Don't show the old textbox NPC
oldTextBoxNPC.style.display = "none";

// Give the editor some height
newTextBoxNPC.style.height = "20em";

// Make the editor take up the full width
oldTextBoxNPC.parentElement.style.flex = "100%";

// Insert the new textbox right after the old one
oldTextBoxNPC.after(newTextBoxNPC);

if (game.modules.get("acelib")?.active) {
// Update whenever the ace editor changes
editor.on("change", () => {
// Copy the value from the ace editor to the old textbox
oldTextBox.value = editor.getValue();
});
editorNPC.on("change", () => {
// Copy the value from the ace editor to the old textbox
oldTextBoxNPC.value = editorNPC.getValue();
});
} else {
// Update whenever the new textbox changes
newTextBox.addEventListener("change", () => {
// Copy the value from the new textbox to the old one
oldTextBox.value = newTextBox.value;
});
newTextBoxNPC.addEventListener("change", () => {
// Copy the value from the new textbox to the old one
oldTextBoxNPC.value = newTextBoxNPC.value;
});
}

// Create mapping select menu
Expand All @@ -116,6 +175,28 @@ Hooks.on("renderSettingsConfig", (app, html) => {
});
});

// Create mapping select menu NPC
const mappingSelectNPC = document.createElement("select");
mappingSelectNPC.style.margin = "10px 0";
oldTextBoxNPC.parentNode.before(mappingSelectNPC);

// Browse and get list of included mapping files
FilePicker.browse("data", "modules/pdf-sheet/mappings", { extensions: [".mapping"] }).then(results => {
// Add the default option first
results.files.unshift("");

// Add options for each included mapping
results.files.forEach(name => {
// Create the option
const option = document.createElement("option");
mappingSelectNPC.append(option);

// Add just the name of the system as the text
name = name.split("/").at(-1).replace(".mapping", "");
option.innerHTML = name;
});
});

// Resize the Settings Config App
app.setPosition();

Expand All @@ -138,13 +219,34 @@ Hooks.on("renderSettingsConfig", (app, html) => {
newTextBox.value = mapping;
}
});
mappingSelectNPC.addEventListener("change", async () => {
// Fetch selected mapping if not empty
const mapping = mappingSelectNPC.value
? await fetch(getRoute(`/modules/pdf-sheet/mappings/${mappingSelectNPC.value}.mapping`)).then(response =>
response.text()
)
: "";

// Copy the mapping to the old text box
oldTextBoxNPC.value = mapping;
if (game.modules.get("acelib")?.active) {
// Copy the mapping to the ace editor
editorNPC.setValue(mapping);
} else {
// Copy the mapping to the new textbox
newTextBoxNPC.value = mapping;
}
});
}
});

// Add button to Actor Sheet for opening app
Hooks.on("getActorSheetHeaderButtons", (sheet, buttons) => {
console.log("check the sheet")
console.log(sheet)
console.log(sheet.actor)
// If this is not a player character sheet, return without adding the button
if (!["character", "PC", "Player"].includes(sheet.actor.type ?? sheet.actor.data.type)) return;
if (!["character", "PC", "Player", "npc"].includes(sheet.actor.type ?? sheet.actor.data.type)) return;

buttons.unshift({
label: "Export to PDF",
Expand Down Expand Up @@ -237,11 +339,22 @@ class Pdfconfig extends FormApplication {
console.log("PDF fields:", pdfFields);

// Get mapping from settings
let mapping = game.settings.get(Pdfconfig.ID, "mapping");
let mapping = ""

if (["character", "PC", "Player"].includes(actor.type ?? actor.data.type)) {
console.log("got mapping for PC")
mapping = game.settings.get(Pdfconfig.ID, "mapping");
}
if (["npc"].includes(actor.type ?? actor.data.type)) {
console.log("got mapping for NPC")
mapping = game.settings.get(Pdfconfig.ID, "mapping-npc");
}


// Parse dynamic keys
mapping = mapping.replaceAll("@", game.release.generation > 10 ? "actor." : "actor.data.");


// Log un-evaluated mapping
console.log("Raw mapping:", mapping);

Expand Down

0 comments on commit dacc18b

Please sign in to comment.