const MODULE_ID = "dh-environment-overlay"; const FLAG_KEY = "environmentUuid"; Hooks.once("init", () => { console.log(`${MODULE_ID} | Initializing Daggerheart Environment Overlay`); // Register Menu game.settings.registerMenu(MODULE_ID, "overlayConfig", { name: "Overlay Configuration", label: "Configure Overlay Style", hint: "Customize the size, shape, and color of the environment overlay.", icon: "fas fa-palette", type: EnvironmentOverlayConfig, restricted: true }); game.settings.register(MODULE_ID, "borderColor", { name: "Overlay Border Color", scope: "world", config: false, type: String, default: "#f3c267", onChange: () => renderEnvironmentOverlay() }); game.settings.register(MODULE_ID, "iconSize", { name: "Icon Size", scope: "world", config: false, type: Number, default: 80, onChange: () => renderEnvironmentOverlay() }); game.settings.register(MODULE_ID, "iconShape", { name: "Icon Shape", scope: "world", config: false, type: String, default: "rounded", onChange: () => renderEnvironmentOverlay() }); game.settings.register(MODULE_ID, "showName", { name: "Show Actor Name", scope: "world", config: false, type: Boolean, default: true, onChange: () => renderEnvironmentOverlay() }); }); /** * Configuration Application */ /** * Configuration Application */ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; class EnvironmentOverlayConfig extends HandlebarsApplicationMixin(ApplicationV2) { static DEFAULT_OPTIONS = { tag: "form", id: "dh-environment-overlay-config", window: { title: "Environment Overlay Configuration", icon: "fas fa-palette", resizable: false }, position: { width: 500, height: "auto" }, classes: ["daggerheart", "dialog", "dh-style", "module"], actions: { reset: EnvironmentOverlayConfig.onReset }, form: { handler: EnvironmentOverlayConfig.onSubmit, closeOnSubmit: true } }; static PARTS = { form: { template: "modules/dh-environment-overlay/templates/overlay-config.hbs" } }; async _prepareContext(options) { return { borderColor: game.settings.get(MODULE_ID, "borderColor"), iconSize: game.settings.get(MODULE_ID, "iconSize"), iconShape: game.settings.get(MODULE_ID, "iconShape"), showName: game.settings.get(MODULE_ID, "showName"), shapes: { "rounded": "Rounded Square", "square": "Square", "circle": "Circle" } }; } _onRender(context, options) { super._onRender(context, options); // Update range value display const html = this.element; const rangeInput = html.querySelector('input[name="iconSize"]'); const rangeValue = html.querySelector('.range-value'); if (rangeInput && rangeValue) { rangeInput.addEventListener('input', (event) => { rangeValue.textContent = event.target.value; }); } } static async onReset(event, target) { await game.settings.set(MODULE_ID, "borderColor", "#f3c267"); await game.settings.set(MODULE_ID, "iconSize", 80); await game.settings.set(MODULE_ID, "iconShape", "rounded"); await game.settings.set(MODULE_ID, "showName", true); this.render(); ui.notifications.info("Environment Overlay settings reset to defaults."); } static async onSubmit(event, form, formData) { const data = formData.object; for (let [key, value] of Object.entries(data)) { await game.settings.set(MODULE_ID, key, value); } } } /** * Handle Scene Configuration Render */ Hooks.on("renderSceneConfig", async (app, html, data) => { // Determine if html is a jQuery object or HTMLElement const $html = html instanceof HTMLElement ? $(html) : html; // Target the Daggerheart specific tab const dhTab = $html.find('.tab[data-tab="dh"]'); const environmentUuid = app.document.getFlag(MODULE_ID, FLAG_KEY); let currentActor = null; if (environmentUuid) { currentActor = await fromUuid(environmentUuid); } const dropZoneHtml = `
${currentActor ? `
${currentActor.name}
` : `
Drag & Drop Environment Actor Here
`}

Link a Daggerheart Environment Actor to this scene. Its token will appear as an overlay.

`; // Inject into the DH tab if it exists if (dhTab.length > 0) { dhTab.append(dropZoneHtml); } else { // Fallback: Try to put it in the first tab or top of form const form = $html.find("form"); const nameGroup = $html.find("input[name='name']").closest(".form-group"); if (nameGroup.length > 0) { nameGroup.after(dropZoneHtml); } else { form.prepend(dropZoneHtml); } } const dropZone = $html.find(".dh-environment-wrapper"); // Add drag/drop listeners if (dropZone.length === 0) return; dropZone[0].addEventListener("dragover", (event) => { event.preventDefault(); dropZone.find(".dh-environment-drop-zone").addClass("drag-hover"); }); dropZone[0].addEventListener("dragleave", (event) => { event.preventDefault(); dropZone.find(".dh-environment-drop-zone").removeClass("drag-hover"); }); dropZone[0].addEventListener("drop", async (event) => { event.preventDefault(); dropZone.find(".dh-environment-drop-zone").removeClass("drag-hover"); try { const data = TextEditor.getDragEventData(event); if (data.type !== "Actor") return; const actor = await fromUuid(data.uuid); if (!actor || actor.type !== "environment") { ui.notifications.warn("DH Environment Overlay | Please drop a valid 'Environment' type Actor."); return; } updateDropZoneDisplay(dropZone, actor); } catch (err) { console.error(err); } }); // Handle Remove dropZone.on("click", ".dh-environment-remove", () => { updateDropZoneDisplay(dropZone, null); }); // Helper to update display and hidden input function updateDropZoneDisplay(wrapper, actor) { // Find or create hidden input let input = wrapper.find(`input[name="flags.${MODULE_ID}.${FLAG_KEY}"]`); if (input.length === 0) { input = $(``); wrapper.append(input); } input.val(actor ? actor.uuid : ""); const content = actor ? `
${actor.name}
` : `
Drag & Drop Environment Actor Here
`; wrapper.children().not("input").remove(); wrapper.append(content); } // Ensure hidden input exists for initial validation if null let existingInput = $html.find(`input[name="flags.${MODULE_ID}.${FLAG_KEY}"]`); if (existingInput.length === 0) { dropZone.append(``); } // Force resize because we added content app.setPosition({ height: "auto" }); }); /** * Handle Overlay Rendering */ async function renderEnvironmentOverlay() { // Remove existing const existing = $("#dh-environment-overlay"); if (existing.length) existing.remove(); if (!canvas.scene) return; const environmentUuid = canvas.scene.getFlag(MODULE_ID, FLAG_KEY); if (!environmentUuid) return; try { const actor = await fromUuid(environmentUuid); if (!actor) return; // Maybe deleted? // PERMISSION CHECK if (!actor.testUserPermission(game.user, "LIMITED")) { return; } const position = canvas.scene.getFlag(MODULE_ID, "overlayPosition") || { top: 100, right: 320 }; const borderColor = game.settings.get(MODULE_ID, "borderColor"); const showName = game.settings.get(MODULE_ID, "showName"); // New Settings const iconSize = game.settings.get(MODULE_ID, "iconSize"); const iconShape = game.settings.get(MODULE_ID, "iconShape"); let borderRadius = "20%"; // Default rounded if (iconShape === "circle") borderRadius = "50%"; if (iconShape === "square") borderRadius = "0"; let styleStr = ""; if (position.left !== undefined) styleStr += `left: ${position.left}px;`; else if (position.right !== undefined) styleStr += `right: ${position.right}px;`; if (position.top !== undefined) styleStr += `top: ${position.top}px;`; else if (position.bottom !== undefined) styleStr += `bottom: ${position.bottom}px;`; // Set width on container to ensure centering logic still works if relied on width styleStr += `width: ${iconSize}px; --dh-overlay-color: ${borderColor};`; const overlay = $(`
${showName ? `
${actor.name}
` : ""}
`); $("body").append(overlay); overlay.on("click", (event) => { // Prevent opening sheet if we just dragged if (overlay.data("isDragging")) return; actor.sheet.render(true); }); // Drag Logic overlay.on("mousedown", (event) => { if (!event.altKey) return; event.preventDefault(); event.stopPropagation(); overlay.data("isDragging", true); // Get initial cursor offset relative to element const rect = overlay[0].getBoundingClientRect(); const offsetX = event.clientX - rect.left; const offsetY = event.clientY - rect.top; const moveHandler = (moveEvent) => { const x = moveEvent.clientX - offsetX; const y = moveEvent.clientY - offsetY; // Update specific styles to override CSS class overlay.css({ left: `${x}px`, top: `${y}px`, right: 'auto', bottom: 'auto' }); }; const upHandler = async (upEvent) => { $(document).off("mousemove", moveHandler); $(document).off("mouseup", upHandler); // Small delay to prevent click trigger setTimeout(() => overlay.data("isDragging", false), 50); const rect = overlay[0].getBoundingClientRect(); const newPos = { left: rect.left, top: rect.top }; await canvas.scene.setFlag(MODULE_ID, "overlayPosition", newPos); }; $(document).on("mousemove", moveHandler); $(document).on("mouseup", upHandler); }); } catch (err) { console.warn(`${MODULE_ID} | Failed to load environment actor:`, err); } } Hooks.on("canvasReady", renderEnvironmentOverlay); Hooks.on("updateScene", (document, change, options, userId) => { if (!document.isView) return; if (canvas.scene && document.id === canvas.scene.id) { if (foundry.utils.hasProperty(change, `flags.${MODULE_ID}`)) { const flags = change.flags[MODULE_ID]; if (flags) { if (flags[FLAG_KEY] !== undefined) { renderEnvironmentOverlay(); } else if (flags.overlayPosition && userId !== game.user.id) { renderEnvironmentOverlay(); } } } } });