Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- async function positionManagerMacro() {
- const scene = canvas.scene;
- const FLAG_SCOPE = "world";
- const FLAG_KEY = "positionSnapshots";
- const getSnapshots = () => scene.getFlag(FLAG_SCOPE, FLAG_KEY) || {};
- const style = `
- <style>
- .pos-manager-ui { background: #002366 !important; color: #fff !important; border: 3px solid #87CEEB !important; border-radius: 8px; }
- .pos-manager-ui .window-content { background: #002366 !important; padding: 20px; color: #fff !important; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; }
- .pos-manager-ui .window-header { background: #001a4d !important; border-bottom: 2px solid #87CEEB !important; }
- .pos-manager-ui .window-title { color: #fff !important; font-weight: bold; text-transform: uppercase; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; }
- /* Side-by-Side Filter Bar */
- .filter-container {
- background: rgba(135, 206, 235, 0.1);
- padding: 12px;
- margin-bottom: 15px;
- border-radius: 4px;
- border: 1px solid #87CEEB;
- display: flex;
- justify-content: space-around;
- gap: 20px;
- }
- .filter-row { display: flex; align-items: center; white-space: nowrap; }
- .filter-row label { color: #fff !important; font-weight: 900; cursor: pointer; font-size: 0.85em; }
- .filter-row input { width: 16px; height: 16px; margin-right: 8px; cursor: pointer; }
- .snapshot-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; margin-bottom: 10px; background: rgba(0, 0, 0, 0.3); border: 1px solid #87CEEB; cursor: grab; }
- .snapshot-item.dragging { opacity: 0.5; background: #0047ab; }
- .snapshot-name { color: #fff !important; font-weight: 900; font-size: 1.2em; flex: 1; pointer-events: none; }
- .action-btn { cursor: pointer; font-weight: 900; padding: 8px 14px; border-radius: 4px; margin-left: 5px; border: 1px solid #fff; font-size: 0.8em; text-shadow: none; text-transform: uppercase; }
- .load-btn { background: #0047ab; color: #fff !important; }
- .rename-btn { background: #444; color: #fff !important; }
- .del-btn { background: #8b0000; color: #fff !important; }
- .save-footer-btn { display: block; width: 100%; padding: 16px; margin-top: 20px; background: #005A9C !important; border: 2px solid #87CEEB !important; border-radius: 4px; color: #fff !important; font-weight: 900 !important; cursor: pointer; text-align: center; text-transform: uppercase; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; }
- .pos-manager-ui .dialog-buttons button { background: #003366 !important; color: #fff !important; border: 1px solid #87CEEB !important; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; }
- input#snapshot-name, input#rename-input { background: #001a4d !important; color: #fff !important; border: 2px solid #87CEEB !important; padding: 12px !important; font-size: 1.2em; width: 100%; }
- </style>`;
- const renderDashboard = () => {
- const snapshots = getSnapshots();
- const listHtml = Object.keys(snapshots).map(name => `
- <div class="snapshot-item" draggable="true" data-name="${name}">
- <span class="snapshot-name">☰ ${name}</span>
- <div style="display: flex;">
- <a class="action-btn load-btn" data-name="${name}">Load</a>
- <a class="action-btn rename-btn" data-name="${name}">Rename</a>
- <a class="action-btn del-btn" data-name="${name}">Delete</a>
- </div>
- </div>`).join("") || "<p style='text-align:center;'>No positions saved.</p>";
- new Dialog({
- title: `Snapshot for ${scene.name}`,
- content: `${style}
- <div class="filter-container">
- <div class="filter-row">
- <input type="checkbox" id="load-skip-pcs" checked>
- <label for="load-skip-pcs">Don't Move Players</label>
- </div>
- <div class="filter-row">
- <input type="checkbox" id="load-skip-combatants">
- <label for="load-skip-combatants">Don't Move Combatants</label>
- </div>
- </div>
- <div id="snapshot-list" style="max-height: 400px; overflow-y: auto;">${listHtml}</div>
- <button class="save-footer-btn" id="trigger-save-new">💾 Save new snapshot</button>`,
- buttons: {},
- render: (html) => {
- // --- DRAG AND DROP ---
- const list = html.find('#snapshot-list')[0];
- let draggingElement = null;
- html.find('.snapshot-item').on('dragstart', (ev) => {
- draggingElement = ev.currentTarget;
- ev.currentTarget.classList.add('dragging');
- });
- html.find('.snapshot-item').on('dragend', async (ev) => {
- ev.currentTarget.classList.remove('dragging');
- const newOrder = {};
- html.find('.snapshot-item').each((i, el) => {
- const name = el.dataset.name;
- newOrder[name] = snapshots[name];
- });
- await scene.setFlag(FLAG_SCOPE, FLAG_KEY, newOrder);
- });
- list.addEventListener('dragover', (ev) => {
- ev.preventDefault();
- const draggableElements = [...list.querySelectorAll('.snapshot-item:not(.dragging)')];
- const afterElement = draggableElements.reduce((closest, child) => {
- const box = child.getBoundingClientRect();
- const offset = ev.clientY - box.top - box.height / 2;
- if (offset < 0 && offset > closest.offset) return { offset: offset, element: child };
- return closest;
- }, { offset: Number.NEGATIVE_INFINITY }).element;
- if (afterElement == null) list.appendChild(draggingElement);
- else list.insertBefore(draggingElement, afterElement);
- });
- // --- BUTTONS ---
- html.find('.load-btn').click(async (ev) => {
- const skipPCs = html.find('#load-skip-pcs').is(':checked');
- const skipCombatants = html.find('#load-skip-combatants').is(':checked');
- const data = snapshots[ev.currentTarget.dataset.name];
- const combatantIds = game.combat?.combatants.map(c => c.tokenId) || [];
- let updates = canvas.tokens.placeables.filter(t => data[t.id]).map(t => {
- const isPC = t.actor?.type === "character";
- const isCombatant = combatantIds.includes(t.id);
- return { _id: t.id, ...data[t.id], isPC, isCombatant };
- });
- if (skipPCs) updates = updates.filter(u => !u.isPC);
- if (skipCombatants) updates = updates.filter(u => !u.isCombatant);
- if (updates.length > 0) {
- const finalUpdates = updates.map(({isPC, isCombatant, ...keep}) => keep);
- await scene.updateEmbeddedDocuments("Token", finalUpdates);
- }
- });
- html.find('.rename-btn').click((ev) => {
- html.closest('.pos-manager-ui').remove();
- renderRenamePrompt(ev.currentTarget.dataset.name);
- });
- html.find('.del-btn').click(async (ev) => {
- const s = getSnapshots(); delete s[ev.currentTarget.dataset.name];
- await scene.unsetFlag(FLAG_SCOPE, FLAG_KEY);
- await scene.setFlag(FLAG_SCOPE, FLAG_KEY, s);
- html.closest('.pos-manager-ui').remove(); renderDashboard();
- });
- html.find('#trigger-save-new').click(() => { html.closest('.pos-manager-ui').remove(); renderSavePrompt(); });
- }
- }, { classes: ["dialog", "pos-manager-ui"], width: 650 }).render(true);
- };
- const renderRenamePrompt = (oldName) => {
- const dialog = new Dialog({
- title: `Rename: ${oldName}`,
- content: `${style}<p style="font-weight:900; margin-bottom:10px;">Enter New Name:</p><input type="text" id="rename-input" value="${oldName}" autofocus>`,
- buttons: {
- confirm: { label: "Confirm", callback: (html) => executeRename(html) },
- cancel: { label: "Cancel", callback: () => renderDashboard() }
- },
- render: (html) => {
- html.find('#rename-input').on('keydown', (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- html.find('.dialog-button.confirm').click();
- }
- });
- }
- }, { classes: ["dialog", "pos-manager-ui"], width: 400 });
- async function executeRename(html) {
- const newName = html.find('#rename-input').val();
- if (!newName || newName === oldName) return renderDashboard();
- const s = getSnapshots();
- const updatedSnapshots = {};
- for (let [key, value] of Object.entries(s)) {
- updatedSnapshots[key === oldName ? newName : key] = value;
- }
- await scene.unsetFlag(FLAG_SCOPE, FLAG_KEY);
- await scene.setFlag(FLAG_SCOPE, FLAG_KEY, updatedSnapshots);
- renderDashboard();
- }
- dialog.render(true);
- };
- const renderSavePrompt = () => {
- const dialog = new Dialog({
- title: "Save Snapshot",
- content: `${style}<p style="font-weight:900; margin-bottom:10px;">Enter Snapshot Name:</p><input type="text" id="snapshot-name" autofocus>`,
- buttons: {
- save: { label: "Save", callback: (html) => executeSave(html) },
- cancel: { label: "Cancel", callback: () => renderDashboard() }
- },
- render: (html) => {
- html.find('#snapshot-name').on('keydown', (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- html.find('.dialog-button.save').click();
- }
- });
- }
- }, { classes: ["dialog", "pos-manager-ui"], width: 400 });
- async function executeSave(html) {
- const name = html.find('#snapshot-name').val() || `Snapshot ${new Date().toLocaleTimeString()}`;
- const s = getSnapshots();
- const current = {};
- canvas.tokens.placeables.forEach(t => {
- current[t.id] = {
- x: t.document.x, y: t.document.y, rotation: t.document.rotation, hidden: t.document.hidden
- };
- });
- s[name] = current;
- await scene.setFlag(FLAG_SCOPE, FLAG_KEY, s);
- renderDashboard();
- }
- dialog.render(true);
- };
- renderDashboard();
- }
- positionManagerMacro();
Advertisement