Skip to content

Instantly share code, notes, and snippets.

@scooper4711
Created November 28, 2025 04:32
Show Gist options
  • Select an option

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

Select an option

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/
// 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