Skip to content

Instantly share code, notes, and snippets.

@scooper4711
Last active November 30, 2025 13:56
Show Gist options
  • Select an option

  • Save scooper4711/8410a9e4a2ddd8811c73930fc33c9883 to your computer and use it in GitHub Desktop.

Select an option

Save scooper4711/8410a9e4a2ddd8811c73930fc33c9883 to your computer and use it in GitHub Desktop.
Exports selected tokens to a TSV format suitable for import into SageRPG
// FoundryVTT Macro: Export Selected Tokens to TSV
// This macro exports selected token data to a tab-separated value format
(async () => {
// Check if any tokens are selected
let selectedTokens = canvas.tokens.controlled;
if (selectedTokens.length === 0) {
ui.notifications.warn("Please select at least one token");
return;
}
// Sort tokens by actor name and remove duplicates
selectedTokens = selectedTokens
.slice() // copy
.sort((a, b) => {
const nameA = (a.document.name || "").toLowerCase();
const nameB = (b.document.name || "").toLowerCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
const seenNames = new Set();
selectedTokens = selectedTokens.filter(token => {
const name = token.document.name;
if (!name) return false;
if (seenNames.has(name)) return false;
seenNames.add(name);
return true;
});
// TSV Header (added hp, maxHp after ac; abilities str,dex,con,int,wis,cha; skills; then token/avatar/color)
const headers = ["type", "name", "gameSystem", "level", "ac", "hp", "maxHp", "perception", "fort", "ref", "will", "str", "dex", "con", "int", "wis", "cha", "acrobatics", "arcana", "athletics", "crafting", "deception", "diplomacy", "intimidation", "medicine", "nature", "occultism", "performance", "religion", "society", "stealth", "survival", "thievery", "token", "avatar", "color"];
const rows = [headers.join("\t")];
// Process each selected token
for (const token of selectedTokens) {
const actor = token.actor;
if (!actor) continue;
// Determine type (npc or pc)
const type = actor.type === "npc" ? "npc" : "pc";
// Get name (for NPCs use token name, for PCs use actor name)
const name = type === "npc" ? token.document.name : actor.name;
// Game system
const gameSystem = game.system.id;
// Get level
let level = actor.system.details?.level?.value ?? "";
// Get AC (Armor Class) and HP values
let ac = actor.system.attributes?.ac?.value ?? "";
const hpAttr = actor.system.attributes?.hp || {};
let hp = hpAttr.value ?? "";
let maxHp = hpAttr.max ?? "";
// Get saves (Fortitude, Reflex, Will)
let fort = actor.system.saves?.fortitude?.value ?? "";
let ref = actor.system.saves?.reflex?.value ?? "";
let will = actor.system.saves?.will?.value ?? "";
// Ability scores (PF2e stores as str, dex, con, int, wis, cha with .mod property)
const abilities = actor.system.abilities || {};
function getAbility(abbrev) {
const a = abilities[abbrev];
if (a && (a.mod !== undefined && a.mod !== null)) {
return a.mod;
}
return "";
}
let str = getAbility("str");
let dex = getAbility("dex");
let con = getAbility("con");
let int = getAbility("int");
let wis = getAbility("wis");
let cha = getAbility("cha");
// Skills (PF2e stores with .value property for modifier)
const skills = actor.system.skills || {};
// For perception, use actor.system.perception.mod for NPCs, skills["perception"].value for PCs
let perception = "";
function getSkill(skillName) {
if (type === "npc") {
// NPCs: use .mod for skills (handles Weak/Elite adjustments), .mod for perception
if (skillName === "perception") {
return actor.system.perception?.mod ?? "";
}
return skills[skillName]?.mod ?? "";
} else {
// PCs: use .value for skills and perception
if (skillName === "perception") {
return skills[skillName]?.value ?? "";
}
return skills[skillName]?.value ?? "";
}
}
perception = getSkill("perception");
let acrobatics = getSkill("acrobatics");
let arcana = getSkill("arcana");
let athletics = getSkill("athletics");
let crafting = getSkill("crafting");
let deception = getSkill("deception");
let diplomacy = getSkill("diplomacy");
let intimidation = getSkill("intimidation");
let medicine = getSkill("medicine");
let nature = getSkill("nature");
let occultism = getSkill("occultism");
let performance = getSkill("performance");
let religion = getSkill("religion");
let society = getSkill("society");
let stealth = getSkill("stealth");
let survival = getSkill("survival");
let thievery = getSkill("thievery");
// For NPCs, hide only hp, maxHp, and ac with ||
if (type === "npc") {
function hide(val) {
return (val !== "") ? `||${val}||` : val;
}
ac = hide(ac);
hp = hide(hp);
maxHp = hide(maxHp);
}
// Get token image
let tokenImage = token.document.texture.src || "";
if (tokenImage && !tokenImage.startsWith("http")) {
tokenImage = window.location.origin + "/" + tokenImage.replace(/^\//, "");
}
// Get avatar image (actor's portrait)
let avatar = actor.img || "";
if (avatar && !avatar.startsWith("http")) {
avatar = window.location.origin + "/" + avatar.replace(/^\//, "");
}
// Get owner's color (find first owner who isn't GM)
let color = "0xFFFFFF"; // Default white
const ownership = actor.ownership || {};
for (const [userId, level] of Object.entries(ownership)) {
if (level >= 3 && userId !== "default") { // OWNER level
const user = game.users.get(userId);
if (user && !user.isGM) {
const userColor = user.color ? String(user.color) : "#FFFFFF";
color = userColor.replace("#", "0x");
break;
}
}
}
// Create row matching header order
const row = [
type,
name,
gameSystem,
level,
ac,
hp,
maxHp,
perception,
fort,
ref,
will,
str,
dex,
con,
int,
wis,
cha,
acrobatics,
arcana,
athletics,
crafting,
deception,
diplomacy,
intimidation,
medicine,
nature,
occultism,
performance,
religion,
society,
stealth,
survival,
thievery,
tokenImage,
avatar,
color
].join("\t");
rows.push(row);
}
// Combine all rows
const tsvContent = rows.join("\n");
// Create a dialog to display and copy the TSV content
const dialog = new Dialog({
title: "Export Tokens to TSV",
content: `
<div style="margin-bottom: 10px;">
<p>TSV data for ${selectedTokens.length} token(s) generated. Copy the content below:</p>
</div>
<textarea id="tsv-output" readonly style="width: 100%; height: 300px; font-family: monospace; font-size: 12px;">${tsvContent}</textarea>
`,
buttons: {
copy: {
icon: '<i class="fas fa-copy"></i>',
label: "Copy to Clipboard",
callback: async () => {
await navigator.clipboard.writeText(tsvContent);
ui.notifications.info("TSV data copied to clipboard!");
}
},
download: {
icon: '<i class="fas fa-download"></i>',
label: "Download File",
callback: () => {
const blob = new Blob([tsvContent], { type: 'text/tab-separated-values' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tokens-export-${Date.now()}.tsv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
ui.notifications.info("TSV file downloaded!");
}
},
close: {
icon: '<i class="fas fa-times"></i>',
label: "Close"
}
},
default: "copy",
render: (html) => {
// Auto-select the text area content for easy copying
html.find("#tsv-output").on("click", function() {
this.select();
});
}
});
dialog.render(true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment