Last active
November 30, 2025 13:56
-
-
Save scooper4711/8410a9e4a2ddd8811c73930fc33c9883 to your computer and use it in GitHub Desktop.
Exports selected tokens to a TSV format suitable for import into SageRPG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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