diff --git a/mappings/dnd5eV11-npc.mapping b/mappings/dnd5eV11-npc.mapping new file mode 100644 index 0000000..93bfef0 --- /dev/null +++ b/mappings/dnd5eV11-npc.mapping @@ -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": "" } +] diff --git a/pdf/npc sheet 5e.pdf b/pdf/npc sheet 5e.pdf new file mode 100644 index 0000000..2688155 Binary files /dev/null and b/pdf/npc sheet 5e.pdf differ diff --git a/scripts/pdfsheet.js b/scripts/pdfsheet.js index 259a283..4651306 100644 --- a/scripts/pdfsheet.js +++ b/scripts/pdfsheet.js @@ -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", @@ -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); @@ -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 @@ -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 @@ -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(); @@ -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", @@ -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);