Created
November 28, 2025 04:32
-
-
Save scooper4711/f1c466c03ac4761397842d73e57f425f to your computer and use it in GitHub Desktop.
Exports the current FoundryVTT scene to RPGSage.io map format, for use with https://rpgsage.io/maps/
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 Current Scene to Map File | |
| // This macro exports the current scene data to a map file format | |
| (async () => { | |
| // Get the current scene (not necessarily the active one) | |
| const scene = canvas.scene; | |
| if (!scene) { | |
| ui.notifications.warn("No scene is currently loaded"); | |
| return; | |
| } | |
| // Get scene name | |
| const sceneName = scene.name || "Untitled Scene"; | |
| // Get background image | |
| let backgroundImage = scene.background?.src || ""; | |
| if (backgroundImage && !backgroundImage.startsWith("http")) { | |
| backgroundImage = window.location.origin + "/" + backgroundImage.replace(/^\//, ""); | |
| } | |
| // Get grid dimensions from scene configuration (use sceneWidth/sceneHeight to exclude padding) | |
| const gridSize = scene.grid?.size || 100; | |
| const sceneWidth = scene.dimensions?.sceneWidth || 0; | |
| const sceneHeight = scene.dimensions?.sceneHeight || 0; | |
| const sceneX = scene.dimensions?.sceneX || 0; | |
| const sceneY = scene.dimensions?.sceneY || 0; | |
| const gridSquaresX = Math.round(sceneWidth / gridSize); | |
| const gridSquaresY = Math.round(sceneHeight / gridSize); | |
| // Get default spawn point (center of map if not defined) | |
| const spawnX = Math.round(gridSquaresX / 2); | |
| const spawnY = Math.round(gridSquaresY / 2); | |
| // Build map content | |
| let mapContent = `[map] | |
| ${backgroundImage} | |
| name=${sceneName} | |
| grid=${gridSquaresX}x${gridSquaresY} | |
| spawn=${spawnX},${spawnY}`; | |
| // Get all tokens in the scene | |
| const tokens = scene.tokens; | |
| for (const tokenDoc of tokens) { | |
| // Get token image | |
| let tokenImage = tokenDoc.texture?.src || ""; | |
| if (tokenImage && !tokenImage.startsWith("http")) { | |
| tokenImage = window.location.origin + "/" + tokenImage.replace(/^\//, ""); | |
| } | |
| // Get token name | |
| const tokenName = tokenDoc.name || "Unnamed Token"; | |
| // Get token size in grid squares | |
| const tokenWidth = tokenDoc.width || 1; | |
| const tokenHeight = tokenDoc.height || 1; | |
| // Get token position in grid squares (adjust for scene padding offset) | |
| // Grid coordinates are 1-indexed, so add 1 to convert from 0-indexed | |
| const posX = Math.floor((tokenDoc.x - sceneX) / gridSize) + 1; | |
| const posY = Math.floor((tokenDoc.y - sceneY) / gridSize) + 1; | |
| // Get owner's username (find first owner who isn't GM, or use GM if no player owner) | |
| let ownerUsername = ""; | |
| let gmUsername = ""; | |
| const ownership = tokenDoc.actor?.ownership || tokenDoc.ownership || {}; | |
| for (const [userId, level] of Object.entries(ownership)) { | |
| if (level >= 3 && userId !== "default") { // OWNER level | |
| const user = game.users.get(userId); | |
| if (user) { | |
| const discordUsername = user.getFlag("world", "discordUsername") || user.name || ""; | |
| if (user.isGM) { | |
| gmUsername = discordUsername; | |
| } else { | |
| // Found a player owner | |
| ownerUsername = discordUsername; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // If no player owner found, use GM | |
| if (!ownerUsername && gmUsername) { | |
| ownerUsername = gmUsername; | |
| } | |
| // Build token section | |
| mapContent += `\n\n[token] | |
| ${tokenImage} | |
| name=${tokenName} | |
| size=${tokenWidth}x${tokenHeight} | |
| position=${posX},${posY}`; | |
| if (ownerUsername) { | |
| mapContent += `\nuser=@${ownerUsername}`; | |
| } | |
| } | |
| // Create filename | |
| const fileName = `${sceneName}.map.txt`; | |
| // Create a dialog to display and copy/download the content | |
| const dialog = new Dialog({ | |
| title: "Export Scene to Map File", | |
| content: ` | |
| <div style="margin-bottom: 10px;"> | |
| <p>Map data for scene "${sceneName}" generated. Copy or download below:</p> | |
| </div> | |
| <textarea id="map-output" readonly style="width: 100%; height: 200px; font-family: monospace; font-size: 12px;">${mapContent}</textarea> | |
| `, | |
| buttons: { | |
| copy: { | |
| icon: '<i class="fas fa-copy"></i>', | |
| label: "Copy to Clipboard", | |
| callback: async () => { | |
| await navigator.clipboard.writeText(mapContent); | |
| ui.notifications.info("Map data copied to clipboard!"); | |
| } | |
| }, | |
| download: { | |
| icon: '<i class="fas fa-download"></i>', | |
| label: "Download File", | |
| callback: () => { | |
| const blob = new Blob([mapContent], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = fileName; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| ui.notifications.info(`Map file "${fileName}" downloaded!`); | |
| } | |
| }, | |
| close: { | |
| icon: '<i class="fas fa-times"></i>', | |
| label: "Close" | |
| } | |
| }, | |
| default: "download", | |
| render: (html) => { | |
| // Auto-select the text area content for easy copying | |
| html.find("#map-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