```` <%* const modalForm = app.plugins.plugins.modalforms.api; const dv = app.plugins.plugins.dataview ? app.plugins.plugins.dataview.api : null; if (!dv) { new Notice("Dataview plugin is not enabled."); return; } const result = await modalForm.openForm("New NPC"); if (!result) { new Notice("NPC creation cancelled."); return; } // ─── Function to Convert Comma-Separated Strings into Wiki-Links ───── function toWikiLinks(fieldValue) { return (fieldValue || "") .split(",") .map(item => `[[${item.trim()}]]`) .join(", "); } // ─── Process NPC Basic Data ────────────────────────────────────────── let npcName = result.Name ? String(result.Name).trim() : "Unnamed NPC"; if (npcName.toLowerCase().endsWith(".md")) { npcName = npcName.slice(0, -3); } let notePath = `People/Non-Player Characters/${npcName}`; // UPDATE THIS TO WHEREVER YOU WANT THE NEW NOTES TO BE CREATED let useStatBlock = String(result.get("Stats") || "").toLowerCase() === "true"; // ─── Retrieve NPC Level ─────────────────────────────────────────────── let npcLevel = parseInt(result.get("Level"), 10) || 1; // ─── Ability Scores & Derived Stats ─────────────────────────────────── function rollAbilityScore() { let rolls = [ Math.floor(Math.random() * 6) + 1, Math.floor(Math.random() * 6) + 1, Math.floor(Math.random() * 6) + 1, Math.floor(Math.random() * 6) + 1 ]; rolls.sort((a, b) => a - b); return rolls[1] + rolls[2] + rolls[3] + Math.floor(npcLevel / 2); // Level-based boost } let npc_str = result.get("npc_STR") || rollAbilityScore(); let npc_dex = result.get("npc_DEX") || rollAbilityScore(); let npc_con = result.get("npc_CON") || rollAbilityScore(); let npc_int = result.get("npc_INT") || rollAbilityScore(); let npc_wis = result.get("npc_WIS") || rollAbilityScore(); let npc_cha = result.get("npc_CHA") || rollAbilityScore(); function abilityModifier(score) { return Math.floor((score - 10) / 2); } let bonusAC = Math.floor(npcLevel / 5); let npc_ac = result.get("npc_ac") || (10 + abilityModifier(npc_dex) + bonusAC); let totalHP = 0; for (let i = 0; i < npcLevel; i++) { totalHP += (Math.floor(Math.random() * 8) + 1); } totalHP += npcLevel * abilityModifier(npc_con); let npc_hp = result.get("npc_hp") || totalHP; let npc_hit_dice = `${npcLevel}d8+${npcLevel * abilityModifier(npc_con)}`; // ─── Spells Processing for Frontmatter ────────────────────────────── let spellsInput = result.get("Known Spells") || ""; let spellsArray; try { spellsArray = JSON.parse(spellsInput); } catch (e) { spellsArray = spellsInput.split(",").map(s => s.trim()); } spellsArray = spellsArray.map(s => { let clean = s.trim(); if (clean.startsWith("[")) clean = clean.slice(1); if (clean.endsWith("]")) clean = clean.slice(0, -1); clean = clean.replace(/^"+|"+$/g, ''); return clean; }); spellsArray = spellsArray.filter(s => s !== ""); let spellsList = spellsArray.map(s => `"[[${s}]]"`); let spellsYaml = spellsList.map(spell => ` - ${spell}`).join("\n"); function parseArrayToPlainText(fieldValue) { if (!fieldValue) return ""; try { let arr = JSON.parse(fieldValue); if (Array.isArray(arr)) { return arr.join(", "); } else { return fieldValue; } } catch (e) { return fieldValue; } } // ─── Build Frontmatter ───────────────────────────────────────────────── let frontmatter = `--- Name: ${npcName} Species: ${result.get("Species")} Size: ${result.get("Size")} Alignment: ${result.get("Alignment")} Type: ${result.get("Type")} hp: ${npc_hp} ac: ${npc_ac} Strength: ${npc_str} Dexterity: ${npc_dex} Constitution: ${npc_con} Intelligence: ${npc_int} Wisdom: ${npc_wis} Charisma: ${npc_cha} Skill Proficiencies: ${result.get("Proficiency")} ST Proficiencies: ${result.get("Saving Throws")} Languages: Spells: ${spellsYaml} Location: ${result.get("Primary Location")} aliases: ${result.get("Aliases")} Factions: ${result.get("Factions")} Affiliates: ${result.get("Affiliates")} Appearances: ${result.get("Appearences")} ---`; // ─── Build the Main Note Body ───────────────────────────────────────── // Note that Appearances is just plain text here let noteBody = ` # ${npcName} _${result.get("Species")} ${result.get("Class")}, ${result.get("Alignment")} **Appears In:** ${parseArrayToPlainText(result.get("Appearances"))} ${result.get("bio") && result.get("bio").trim() !== "" ? `_${result.get("bio")}_` : ""} --- > [!infobox] > # More Info > **Primary Location:** ${toWikiLinks(result.get("Primary Location"))} > **Aliases:** ${result.get("Aliases")} > **Factions:** ${toWikiLinks(result.get("Factions"))} > **Affiliates:** ${toWikiLinks(result.get("Affiliates"))} > > # Abilities > \`\`\`dataviewjs > const page = dv.current(); > dv.el("div", \` > | STR | DEX | CON | INT | WIS | CHA | > |---------|-------|----------|---|---|---| > | \${Math.floor((page.Strength - 10)/2)} | \${Math.floor((page.Dexterity - 10)/2)} | \${Math.floor((page.Constitution - 10)/2)} | \${Math.floor((page.Intelligence - 10)/2)} | \${Math.floor((page.Wisdom - 10)/2)} | \${Math.floor((page.Charisma - 10)/2)} | > \`); > \`\`\` `; // ─── Build Spells Dataview Block ───────────────────────────────────── let dvBlock = ""; dvBlock += "```dataviewjs\n"; dvBlock += "// This block renders the spells table from the frontmatter Spells field\n"; dvBlock += "const spells = dv.current().Spells;\n"; dvBlock += "if (spells && Array.isArray(spells) && spells.length > 0) {\n"; dvBlock += " const spellTableData = spells.map(spellItem => {\n"; dvBlock += " const spellString = typeof spellItem === 'string' ? spellItem : String(spellItem);\n"; dvBlock += " const match = spellString.match(/^\\[\\[(.*?)\\]\\]$/);\n"; dvBlock += " if (!match) { return { name: spellString, level: 'N/A', casting_time: 'N/A', range: 'N/A', save: '-' }; }\n"; dvBlock += " let linkPart = match[1];\n"; dvBlock += " let [rawTarget, displayName] = linkPart.split('|');\n"; dvBlock += " if (!displayName) {\n"; dvBlock += " displayName = rawTarget.replace(/\\.md$/i, '').split('/').pop();\n"; dvBlock += " }\n"; dvBlock += " rawTarget = rawTarget.replace(/\\.md$/i, '');\n"; dvBlock += " if (rawTarget.startsWith('/')) { rawTarget = rawTarget.slice(1); }\n"; dvBlock += " const spellLink = `<a href='/${rawTarget}.md' class='internal-link'>${displayName}</a>`;\n"; dvBlock += " const spellPage = dv.page(rawTarget);\n"; dvBlock += " return {\n"; dvBlock += " name: spellLink,\n"; dvBlock += " level: spellPage?.level ?? 'N/A',\n"; dvBlock += " casting_time: spellPage?.casting_time ?? 'N/A',\n"; dvBlock += " range: spellPage?.range ?? 'N/A',\n"; dvBlock += " save: spellPage?.save?.trim() ?? '-'\n"; dvBlock += " };\n"; dvBlock += " }).sort((a, b) => a.level - b.level);\n"; dvBlock += " let tableRows = spellTableData.map(spell => `\n <tr>\n <td style='border: 1px solid #ccc; padding: 5px;'>${spell.name}</td>\n <td style='border: 1px solid #ccc; padding: 5px;'>${spell.level}</td>\n <td style='border: 1px solid #ccc; padding: 5px;'>${spell.casting_time}</td>\n <td style='border: 1px solid #ccc; padding: 5px;'>${spell.range}</td>\n <td style='border: 1px solid #ccc; padding: 5px;'>${spell.save}</td>\n </tr>\n `).join('');\n"; dvBlock += " const tableHTML = `\n <table style='width: 100%; border-collapse: collapse;'>\n <thead>\n <tr>\n <th style='border: 1px solid #ccc; padding: 5px;'>Spell</th>\n <th style='border: 1px solid #ccc; padding: 5px;'>Level</th>\n <th style='border: 1px solid #ccc; padding: 5px;'>Casting Time</th>\n <th style='border: 1px solid #ccc; padding: 5px;'>Range</th>\n <th style='border: 1px solid #ccc; padding: 5px;'>Saving Throw</th>\n </tr>\n </thead>\n <tbody>\n ${tableRows}\n </tbody>\n </table>\n `;\n"; dvBlock += " dv.el('div', tableHTML);\n"; dvBlock += "} else { dv.el('div', 'No spells found.'); }\n"; dvBlock += "```"; noteBody += "\n\n### Spells\n" + dvBlock; // ─── Optionally, Build Stat Block ───────────────────────────────────── let statBlock = ""; if (useStatBlock) { statBlock = ` \`\`\`statblock layout: Basic 5e Layout name: ${npcName} size: ${result.get("Size")} type: ${result.get("Type")} alignment: ${result.get("Alignment")} ac: ${npc_ac} hp: ${npc_hp} hit_dice: ${npc_hit_dice} speed: 30 ft. stats: - STR: ${npc_str} - DEX: ${npc_dex} - CON: ${npc_con} - INT: ${npc_int} - WIS: ${npc_wis} - CHA: ${npc_cha} \`\`\` `; } // ─── Combine All Content and Create the Note ───────────────────────── let noteContent = `${frontmatter} ${noteBody} ${statBlock} `; await tp.file.create_new(noteContent, notePath); await new Promise(resolve => setTimeout(resolve, 2000)); let newFile = tp.file.find_tfile(notePath) || app.vault.getAbstractFileByPath(notePath); if (newFile) { app.workspace.openLinkText(newFile.basename, newFile.path, true); } else { new Notice("File not found: " + notePath); } %>