````
<%*
// ─── SETUP: Access APIs ───────────────────────────────────────────────
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-player-character");
if (!result) {
new Notice("Player Character creation cancelled.");
return;
}
// ─── Helper: Remove all double-asterisks (bold markers) ───────────────
function removeBold(str) {
return str.replace(/\*\*/g, "").trim();
}
// ─── Helper: Choose the Correct Subclass ──────────────────────────────
function getSubclass() {
const subclassFields = [
"subclass-cleric",
"subclass-artificer",
"subclass-barbarian",
"subclass-Bard",
"subclass-Druid",
"subclass-Fighter",
"subclass-Monk",
"subclass-Mystic",
"subclass-paladin",
"subclass-ranger",
"subclass-ranger revised",
"subclass-ranger spelless",
"subclass-rogue",
"subclass-sorcerer",
"subclass-warlock",
"subclass-wizard"
];
for (const key of subclassFields) {
const val = result.get(key);
if (val && String(val).trim() !== "") {
return String(val).trim();
}
}
return "";
}
const subclass = getSubclass();
// ─── Helper: Calculate Proficiency Bonus ──────────────────────────────
function getProficiencyBonus(level) {
const lvl = Math.max(1, parseInt(level || "1", 10));
if (lvl <= 4) return 2;
if (lvl <= 8) return 3;
if (lvl <= 12) return 4;
if (lvl <= 16) return 5;
return 6;
}
// ─── Helper: Process a comma-separated list into a JSON array string ─
function processList(value) {
if (!value) return "[]";
let items = value.split(",")
.map(item => item.trim())
.filter(item => item.length > 0);
return JSON.stringify(items);
}
// ─── Helper: Convert a string or array to a wiki link ────────────────
function asWikilink(value) {
if (!value) return "";
// If Modal Forms is returning an array (e.g. ["Aven"]), flatten it:
if (Array.isArray(value)) value = value.join(" ");
const trimmed = value.trim();
return trimmed ? `[[${trimmed}]]` : "";
}
function toYamlList(key, arr) {
if (!arr || arr.length === 0) return `${key}: []`;
const lines = arr.map(item => ` - "${item}"`).join("\n");
return `${key}:\n${lines}`;
}
let name = result.get("Name") ? String(result.get("Name")).trim() : "Unnamed Character";
if (name.toLowerCase().endsWith(".md")) {
name = name.slice(0, -3);
}
// 1. Get the user’s Status and normalize it
let statusValue = result.get("Status") || "";
statusValue = statusValue.trim().toLowerCase();
// 2. Decide which folder to use based on Status
const folder = (statusValue === "inactive") ? "Inactive" : "Active";
// 3. Build the final file path
const filePath = `World/People/Player Characters/${folder}/${name}`;
// character level and compute proficiency bonus.
const level = parseInt(result.get("level") || "1", 10);
const proficiencyBonus = getProficiencyBonus(level);
// ─── Speed from the Chosen Species ───────────────────────────
let speciesName = result.get("species") ? String(result.get("species")).trim() : "";
let speciesSpeed = "";
if (speciesName !== "") {
try {
let speciesPath = `Compendium/Species/${speciesName}.md`;
let speciesFile = app.vault.getAbstractFileByPath(speciesPath);
if (speciesFile) {
let speciesContent = await app.vault.cachedRead(speciesFile);
let lines = speciesContent.split("\n");
for (let line of lines) {
let trimmed = line.trim();
if (trimmed.startsWith("- **Speed")) {
let rawValue = trimmed.split(":").slice(1).join(":").trim();
speciesSpeed = removeBold(rawValue);
break;
}
}
} else {
new Notice("Species file not found: " + speciesPath);
}
} catch (e) {
new Notice("Error reading species note for " + speciesName + ": " + e.message);
}
}
// ─── Retrieve Proficiencies from the Chosen Class Note ────────────────
let className = result.get("pcclass") ? String(result.get("pcclass")).trim() : "";
let weaponProficiencies = "";
let armorProficiencies = "";
let toolProficiencies = "";
let stProficiencies = "";
if (className !== "") {
try {
let classPath = `Compendium/Classes/${className}/${className}.md`;
let classFile = app.vault.getAbstractFileByPath(classPath);
if (classFile) {
let classContent = await app.vault.cachedRead(classFile);
let classLines = classContent.split("\n");
for (let line of classLines) {
let trimmed = line.trim();
if (trimmed.startsWith("**Armor:**")) {
let rawArmor = trimmed.split(":").slice(1).join(":").trim();
armorProficiencies = processList(removeBold(rawArmor));
}
if (trimmed.startsWith("**Weapons:**")) {
let rawWeapons = trimmed.split(":").slice(1).join(":").trim();
weaponProficiencies = processList(removeBold(rawWeapons));
}
if (trimmed.startsWith("**Tools:**")) {
let rawTools = trimmed.split(":").slice(1).join(":").trim();
toolProficiencies = processList(removeBold(rawTools));
}
if (trimmed.startsWith("**Saving Throws:**")) {
let rawSaves = trimmed.split(":").slice(1).join(":").trim();
stProficiencies = processList(removeBold(rawSaves));
}
}
} else {
new Notice("Class file not found: " + classPath);
}
} catch (e) {
new Notice("Error reading class note for " + className + ": " + e.message);
}
}
// ─── Spells: Parse the Raw JSON and Convert to Wiki-Links ─────────────
const rawSpells = result.get("spells");
new Notice("Raw spells = " + JSON.stringify(rawSpells));
// Attempt to parse the raw JSON string from the form
let spellsArray = [];
try {
// rawSpells should be something like: "[\"Absorb Elements\",\"Scrying\"]"
if (rawSpells) {
spellsArray = JSON.parse(rawSpells);
}
} catch (err) {
new Notice("Error parsing spells JSON: " + err.message);
}
// Convert each spell name into [[Spell]]
const wikiLinkedSpells = spellsArray.map(spell => `[[${spell}]]`);
// Build the multi-line YAML
const spellsYaml = toYamlList("Spells", wikiLinkedSpells);
// ─── Build Frontmatter ────────────────────────────────────────────────
const frontmatter = `---
hp: ${result.get("hp") || ""}
ac: ${result.get("ac") || ""}
modifier: ${result.get("modifier") || ""}
level: ${result.get("level") || ""}
Name: ${name}
Species: "${asWikilink(result.get("species"))}"
Class: "${asWikilink(result.get("pcclass"))}"
Subclass: "${asWikilink(subclass)}"
Alignment: ${result.get("Alignment") || ""}
Strength: ${result.get("strength") || ""}
Dexterity: ${result.get("dexterity") || ""}
Constitution: ${result.get("constitution") || ""}
Intelligence: ${result.get("intelligence") || ""}
Wisdom: ${result.get("wisdom") || ""}
Charisma: ${result.get("charisma") || ""}
current_hp: ${result.get("current_hp") || ""}
Armor Class: ${result.get("ac") || ""}
Speed: ${speciesSpeed}
Proficiency Bonus: +${proficiencyBonus}
Skill Proficiencies: ${result.get("Skill Proficiencies") || ""}
ST Proficiencies: ${stProficiencies}
Weapon Proficiencies: ${weaponProficiencies}
Armor Proficiencies: ${armorProficiencies}
Tool Proficiencies: ${toolProficiencies}
key_items: ${Array.isArray(result.get("key_items")) ? JSON.stringify(result.get("key_items")) : (result.get("key_items") || "[]")}
Languages: ${result.get("Languages") || ""}
${spellsYaml}
aliases: ${result.get("aliases") || ""}
appearances: ${Array.isArray(result.get("appearances")) ? JSON.stringify(result.get("appearances")) : (result.get("appearances") || "[]")}
---`;
// ─── Build the Spells Dataview Block ──────────────────────────────────
const spellBlock = `
### Spells
\`\`\`dataviewjs
// This block renders the spells table from the frontmatter Spells field
const spells = dv.current().Spells;
if (spells && Array.isArray(spells) && spells.length > 0) {
const spellTableData = spells.map(spellItem => {
const spellString = typeof spellItem === 'string' ? spellItem : String(spellItem);
// Attempt to extract wiki link
const match = spellString.match(/^\\[\\[(.*?)\\]\\]$/);
if (!match) {
return {
name: spellString,
level: 'N/A',
casting_time: 'N/A',
range: 'N/A',
save: '-'
};
}
let linkPart = match[1];
let [rawTarget, displayName] = linkPart.split('|');
if (!displayName) {
displayName = rawTarget.replace(/\\.md$/i, '').split('/').pop();
}
rawTarget = rawTarget.replace(/\\.md$/i, '');
if (rawTarget.startsWith('/')) {
rawTarget = rawTarget.slice(1);
}
const spellLink = \`<a href='/\${rawTarget}.md' class='internal-link'>\${displayName}</a>\`;
const spellPage = dv.page(rawTarget);
return {
name: spellLink,
level: spellPage?.level ?? 'N/A',
casting_time: spellPage?.casting_time ?? 'N/A',
range: spellPage?.range ?? 'N/A',
save: spellPage?.save?.trim() ?? '-'
};
}).sort((a, b) => {
// Attempt to sort by numeric level if possible
const levelA = parseInt(a.level, 10) || 0;
const levelB = parseInt(b.level, 10) || 0;
return levelA - levelB;
});
let tableRows = spellTableData.map(spell => \`
<tr>
<td style='border: 1px solid #ccc; padding: 5px;'>\${spell.name}</td>
<td style='border: 1px solid #ccc; padding: 5px;'>\${spell.level}</td>
<td style='border: 1px solid #ccc; padding: 5px;'>\${spell.casting_time}</td>
<td style='border: 1px solid #ccc; padding: 5px;'>\${spell.range}</td>
<td style='border: 1px solid #ccc; padding: 5px;'>\${spell.save}</td>
</tr>
\`).join('');
const tableHTML = \`
<table style='width: 100%; border-collapse: collapse;'>
<thead>
<tr>
<th style='border: 1px solid #ccc; padding: 5px;'>Spell</th>
<th style='border: 1px solid #ccc; padding: 5px;'>Level</th>
<th style='border: 1px solid #ccc; padding: 5px;'>Casting Time</th>
<th style='border: 1px solid #ccc; padding: 5px;'>Range</th>
<th style='border: 1px solid #ccc; padding: 5px;'>Saving Throw</th>
</tr>
</thead>
<tbody>
\${tableRows}
</tbody>
</table>
\`;
dv.el('div', tableHTML);
} else {
dv.el('div', 'No spells found.');
}
\`\`\`
`;
// ─── Saving Throws Block ──────────────────────────────────────────────
const savingThrowBlock = `
\`\`\`dataviewjs
const file = dv.current();
const abilities = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"];
const proficiencies = file["ST Proficiencies"] || [];
const proficiencyBonus = Number(file["Proficiency Bonus"]) || 0;
function calculateModifier(score) {
return Math.floor((score - 10) / 2);
}
const rows = abilities.map(ability => {
const value = Number(file[ability]) || 10;
const mod = calculateModifier(value);
const total = proficiencies.includes(ability) ? mod + proficiencyBonus : mod;
return \`| \${ability} | **\${value}** | **+\${mod}** | **+\${total}** |\`;
});
dv.paragraph(\`
| Ability | Value | Modifier | Saving Throw |
|--------------|-------|----------|--------------|
\${rows.join("\\n")}
\`);
\`\`\`
`;
// ─── Assemble Note Content (Frontmatter + Unchanged Body) ─────────────
const noteContent = `${frontmatter}
# \`= this.Name\`
#### *\`= "Level" + " " + this.level + " " + this.Species + " " + this.Class + " (" + this.Subclass + ")"\`*
### ⚔️ Combat Stats
\`\`\`dataviewjs
const file = dv.current();
const armorClass = file["ac"] || "N/A";
const dexterity = file.Dexterity || 10;
const dexModifier = Math.floor((dexterity - 10) / 2);
const hpMax = file["hp"] || "0";
dv.paragraph(\`
| Armor Class | Initiative | HP |
|-------------|------------|------------------|
| **\${armorClass}** | **+\${dexModifier}** | **\${hpMax}** |
\`);
\`\`\`
${savingThrowBlock}
### 🧠 Skills & Proficiencies
\`\`\`dataviewjs
const skills = {
"Acrobatics": "Dexterity", "Animal Handling": "Wisdom", "Arcana": "Intelligence",
"Athletics": "Strength", "Deception": "Charisma", "History": "Intelligence",
"Insight": "Wisdom", "Intimidation": "Charisma", "Investigation": "Intelligence",
"Medicine": "Wisdom", "Nature": "Intelligence", "Perception": "Wisdom",
"Performance": "Charisma", "Persuasion": "Charisma", "Religion": "Intelligence",
"Sleight of Hand": "Dexterity", "Stealth": "Dexterity", "Survival": "Wisdom"
};
const file = dv.current();
const profSkills = file["Skill Proficiencies"] || [];
const profBonus = parseInt(String(file["Proficiency Bonus"] || "").replace("+", ""), 10) || 0;
function calcMod(score) { return Math.floor((score - 10) / 2); }
let rows = Object.entries(skills).map(([skill, ability]) => {
const score = file[ability] || 10;
const baseMod = calcMod(score);
const totalMod = profSkills.includes(skill) ? baseMod + profBonus : baseMod;
const mark = profSkills.includes(skill) ? "⭐" : "";
return \`| \${skill} | **+\${totalMod}** | \${mark} |\`;
});
dv.paragraph(\`
| Skill | Modifier | Proficient |
|------------------|----------|------------|
\${rows.join("\\n")}
\`);
\`\`\`
### ✨ Spells
${spellBlock}
### 🪕 Key Items
\`= this.key_items\`
### 👤 Personal Details
**Status:** NaN
**Height:** NaN m
**Weight:** NaN kg
**Background:** NaN
**Gender:** NaN
**Birthday:** NaN
---
#### Appearance
*Physical description, notable features, and anything special about the character's appearance.*
#### Personality Traits
*Key traits, motivations, and quirks.*
#### Backstory
*Character backstory, origins, and any important life events.*
#### Current Story Threads
*Active missions or ongoing plotlines.*
#### Goals & Ambitions
*Long-term character goals or aspirations.*
#### Allies & Enemies
*Important allies, NPCs, or enemies.*
#### Notes
*Anything else worth remembering.*
#### Fun Facts!
*Quirky details about your character.*
`;
// ─── Create the File ──────────────────────────────────────────────────
await tp.file.create_new(noteContent, filePath);
await new Promise(resolve => setTimeout(resolve, 2000));
let newFile = tp.file.find_tfile(filePath) || app.vault.getAbstractFileByPath(filePath);
if (newFile) {
app.workspace.openLinkText(newFile.basename, newFile.path, true);
} else {
new Notice("File not found: " + filePath);
}
async function createSecondFileFromTemplate(playerName) {
// 1) Locate the second template file by name/path:
const secondTemplateTFile = tp.file.find_tfile("Extras/Templates/playerdash_template");
if (!secondTemplateTFile) {
new Notice("Second template not found: Templates/playerdash_template");
return;
}
// 2) Read the entire template as a string:
let secondTemplateContent = await app.vault.read(secondTemplateTFile);
// 3) Replace any placeholder in that second template, e.g. {{PlayerName}}
// with the actual 'playerName' we already have.
secondTemplateContent = secondTemplateContent.replace(/{{PlayerName}}/g, name);
// 4) Decide where the new note should go:
// Adjust this path/folder name to suit your vault structure.
const secondFilePath = `Extras/PC Dashboards/dash-${name}`;
// 5) Create the new file using the Templater function:
await tp.file.create_new(secondTemplateContent, secondFilePath);
new Notice(`Created extra file for ${name}`);
}
// Finally, actually call that function, passing in the new character's name:
await createSecondFileFromTemplate(name);
%>