diff --git a/lang/en.json b/lang/en.json index 2f7b71d..a6dde5c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -9,5 +9,13 @@ "COSMOS_DH_TWEAKS.Spotlight.cancel": "Cancel Spotlight Request", "COSMOS_DH_TWEAKS.Spotlight.grant": "Grant Spotlight", "COSMOS_DH_TWEAKS.Spotlight.take": "Take Spotlight", - "COSMOS_DH_TWEAKS.Countdown.add": "Add Countdown" + "COSMOS_DH_TWEAKS.Countdown.add": "Add Countdown", + "COSMOS_DH_TWEAKS.Settings.RemoveFearTitleBar.Name": "Remove Fear Tracker Title Bar", + "COSMOS_DH_TWEAKS.Settings.RemoveFearTitleBar.Hint": "Hides the title bar of the Fear Tracker window by default, allowing you to drag it by clicking on empty space.", + "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Name": "Reset Fear Tracker Position", + "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Hint": "Resets the Fear Tracker window back to its default position and size.", + "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.ButtonText": "Reset Position", + "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Success": "Fear tracker position has been reset.", + "COSMOS_DH_TWEAKS.ContextMenu.LockPosition": "Lock Position", + "COSMOS_DH_TWEAKS.ContextMenu.UnlockPosition": "Unlock Position" } diff --git a/scripts/cosmos-dh-tweaks.mjs b/scripts/cosmos-dh-tweaks.mjs index 385c295..15b6a17 100644 --- a/scripts/cosmos-dh-tweaks.mjs +++ b/scripts/cosmos-dh-tweaks.mjs @@ -38,6 +38,303 @@ Hooks.once("init", () => { } }); + game.settings.register("cosmos-dh-tweaks", "removeFearTitleBar", { + name: "COSMOS_DH_TWEAKS.Settings.RemoveFearTitleBar.Name", + hint: "COSMOS_DH_TWEAKS.Settings.RemoveFearTitleBar.Hint", + scope: "client", + config: true, + type: Boolean, + default: true, + onChange: () => { + if (ui.resources) { + ui.resources.updateTitleBarStatus(); + ui.resources.adjustSizeToMaxFear(); + } + } + }); + + game.settings.register("cosmos-dh-tweaks", "lockFearTrackerPosition", { + scope: "client", + config: false, + type: Boolean, + default: false + }); + + game.settings.register("cosmos-dh-tweaks", "resetFearPosition", { + name: "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Name", + hint: "COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Hint", + scope: "client", + config: true, + type: Boolean, + default: false + }); + + // Override the FearTracker class in CONFIG + const OriginalFearTracker = CONFIG.ui.resources; + if (OriginalFearTracker) { + class CosmoDhTweaksFearTracker extends OriginalFearTracker { + async _onRender(context, options) { + await super._onRender(context, options); + + if (!this._eventsBound) { + this._eventsBound = true; + + this.element.addEventListener('contextmenu', (e) => { + this._showContextMenu(e); + }); + + this.element.addEventListener('mousedown', (e) => { + const isLocked = game.settings.get("cosmos-dh-tweaks", "lockFearTrackerPosition"); + if (isLocked) { + if (e.target.closest('header, .window-resize-handle')) { + e.preventDefault(); + e.stopPropagation(); + return; + } + } + + const removeTitleBar = game.settings.get("cosmos-dh-tweaks", "removeFearTitleBar"); + if (removeTitleBar && !isLocked) { + if (e.button !== 0) return; + if (e.target.closest('i, .controls, button, input, a, .window-resize-handle')) return; + + e.preventDefault(); + this._startDrag(e); + } + }, { capture: true }); + } + + this.updateTitleBarStatus(); + this.updateLockStatus(); + this.adjustSizeToMaxFear(); + } + + updateTitleBarStatus() { + if (!this.element) return; + const removeTitleBar = game.settings.get("cosmos-dh-tweaks", "removeFearTitleBar"); + if (removeTitleBar) { + this.element.classList.add("cosmo-no-title-bar"); + } else { + this.element.classList.remove("cosmo-no-title-bar"); + } + } + + updateLockStatus() { + if (!this.element) return; + const isLocked = game.settings.get("cosmos-dh-tweaks", "lockFearTrackerPosition"); + if (isLocked) { + this.element.classList.add("cosmo-position-locked"); + } else { + this.element.classList.remove("cosmo-position-locked"); + } + } + + getLayoutMeasurements() { + const content = this.element.querySelector('.window-content'); + const grid = this.element.querySelector('#resource-fear'); + const skulls = grid ? grid.querySelectorAll('i') : []; + + let skullW = 48; + let skullH = 48; + let gapX = 4; + let gapY = 8; + let padX = 16; + let padY = 16; + let headerH = 0; + + if (content) { + const style = window.getComputedStyle(content); + const parsedPadX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + const parsedPadY = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); + if (!isNaN(parsedPadX)) padX = parsedPadX; + if (!isNaN(parsedPadY)) padY = parsedPadY; + } + + const header = this.element.querySelector('header.window-header'); + if (header && window.getComputedStyle(header).display !== 'none') { + headerH = header.offsetHeight || 30; + } + + if (skulls.length > 0) { + const first = skulls[0]; + const firstRect = first.getBoundingClientRect(); + if (firstRect.width > 0) skullW = firstRect.width; + if (firstRect.height > 0) skullH = firstRect.height; + + if (skulls.length > 1) { + let second = null; + for (let i = 1; i < skulls.length; i++) { + const rect = skulls[i].getBoundingClientRect(); + if (Math.abs(rect.top - firstRect.top) < 2 && rect.width > 0) { + second = rect; + break; + } + } + if (second) { + gapX = second.left - firstRect.right; + } + + let nextRow = null; + for (let i = 1; i < skulls.length; i++) { + const rect = skulls[i].getBoundingClientRect(); + if (rect.top - firstRect.bottom > 2 && rect.width > 0) { + nextRow = rect; + break; + } + } + if (nextRow) { + gapY = nextRow.top - firstRect.bottom; + } + } + } + + return { skullW, skullH, gapX, gapY, padX, padY, headerH }; + } + + calculatePerfectSize(targetWidth, measurements) { + const { skullW, skullH, gapX, gapY, padX, padY, headerH } = measurements; + const display = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).displayFear; + const maxFear = this.maxFear; + + if (display === 'bar') { + const barElement = this.element.querySelector('#resource-fear'); + const barH = barElement ? barElement.getBoundingClientRect().height : 30; + const perfectH = barH + padY + headerH; + const minW = 200; + const perfectW = Math.max(minW, targetWidth || 222); + + return { + width: perfectW, + height: perfectH + }; + } else { + const w = targetWidth || 222; + let cols = Math.round((w - padX + gapX) / (skullW + gapX)); + cols = Math.max(1, Math.min(maxFear, cols)); + + const perfectW = cols * skullW + (cols - 1) * gapX + padX; + const rows = Math.ceil(maxFear / cols); + const perfectH = rows * skullH + (rows - 1) * gapY + padY + headerH; + + return { + width: perfectW, + height: perfectH + }; + } + } + + adjustSizeToMaxFear() { + if (!this.element) return; + + requestAnimationFrame(() => { + if (!this.element) return; + const measurements = this.getLayoutMeasurements(); + const pos = this.position; + + const perfect = this.calculatePerfectSize(pos.width, measurements); + + if (Math.abs(pos.width - perfect.width) > 1 || Math.abs(pos.height - perfect.height) > 1) { + this.setPosition(perfect); + } + }); + } + + setPosition(position = {}) { + if (!this.element) return super.setPosition(position); + + const measurements = this.getLayoutMeasurements(); + if (position.width !== undefined || position.height !== undefined) { + const currentW = position.width !== undefined ? position.width : this.position.width; + const perfect = this.calculatePerfectSize(currentW, measurements); + + position.width = perfect.width; + position.height = perfect.height; + } + + return super.setPosition(position); + } + + _startDrag(e) { + let isDragging = true; + const startX = e.clientX; + const startY = e.clientY; + const pos = this.position; + const startLeft = pos.left; + const startTop = pos.top; + + const onMouseMove = (event) => { + if (!isDragging) return; + const dx = event.clientX - startX; + const dy = event.clientY - startY; + + this.setPosition({ + left: startLeft + dx, + top: startTop + dy + }); + }; + + const onMouseUp = () => { + isDragging = false; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + } + + _showContextMenu(e) { + e.preventDefault(); + const existing = document.querySelector('.cosmo-fear-context-menu'); + if (existing) existing.remove(); + + const isLocked = game.settings.get("cosmos-dh-tweaks", "lockFearTrackerPosition"); + const menu = document.createElement('div'); + menu.className = 'cosmo-fear-context-menu'; + + const lockText = isLocked + ? ` ${game.i18n.localize("COSMOS_DH_TWEAKS.ContextMenu.UnlockPosition")}` + : ` ${game.i18n.localize("COSMOS_DH_TWEAKS.ContextMenu.LockPosition")}`; + + menu.innerHTML = ` + + `; + + menu.style.position = 'fixed'; + menu.style.left = `${e.clientX}px`; + menu.style.top = `${e.clientY}px`; + menu.style.zIndex = 100000; + + document.body.appendChild(menu); + + menu.querySelector('[data-action="toggle-lock"]').addEventListener('click', async (event) => { + event.preventDefault(); + const currentLock = game.settings.get("cosmos-dh-tweaks", "lockFearTrackerPosition"); + await game.settings.set("cosmos-dh-tweaks", "lockFearTrackerPosition", !currentLock); + this.updateLockStatus(); + menu.remove(); + }); + + const closeMenu = (event) => { + if (!menu.contains(event.target)) { + menu.remove(); + document.removeEventListener('click', closeMenu); + document.removeEventListener('contextmenu', closeMenu); + } + }; + setTimeout(() => { + document.addEventListener('click', closeMenu); + document.addEventListener('contextmenu', closeMenu); + }, 0); + } + } + CONFIG.ui.resources = CosmoDhTweaksFearTracker; + } + // Override the Countdown class in CONFIG const OriginalCountdowns = CONFIG.ui.countdowns; if (OriginalCountdowns) { @@ -295,16 +592,20 @@ Hooks.on("refreshToken", (token, options) => { bg.endFill(); container.addChild(bg); - // Draw FontAwesome solid e05d hand sparkles icon - const style = new PIXI.TextStyle({ - fontFamily: '"Font Awesome 6 Free"', - fontWeight: "900", - fontSize: 13, - fill: "#ffffff" - }); - const text = new PIXI.Text("\ue05d", style); - text.anchor.set(0.5); - container.addChild(text); + // FontAwesome hand-sparkles SVG data URI + const handSparklesSvg = ` + + + +`; + const svgDataUri = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(handSparklesSvg))); + const handTexture = PIXI.Texture.from(svgDataUri); + const handSprite = new PIXI.Sprite(handTexture); + handSprite.anchor.set(0.5); + handSprite.width = 13; + handSprite.height = 13 / (640 / 512); + handSprite.position.set(0, -0.5); + container.addChild(handSprite); token.cosmoSpotlightIndicator = token.addChild(container); } @@ -325,3 +626,34 @@ Hooks.on("destroyToken", (token) => { token.cosmoSpotlightIndicator = null; } }); + +Hooks.on("renderSettingsConfig", (app, html, data) => { + const input = html.querySelector + ? html.querySelector('[name="cosmos-dh-tweaks.resetFearPosition"]') + : html.find('[name="cosmos-dh-tweaks.resetFearPosition"]')[0]; + + if (input) { + const formGroup = input.closest('.form-group'); + if (formGroup) { + const formFields = formGroup.querySelector('.form-fields'); + if (formFields) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'cosmo-reset-fear-pos-btn'; + button.innerHTML = ` ${game.i18n.localize("COSMOS_DH_TWEAKS.Settings.ResetFearPosition.ButtonText")}`; + button.addEventListener('click', async (e) => { + e.preventDefault(); + await game.user.unsetFlag(CONFIG.DH.id, 'app.resources.position'); + if (ui.resources && ui.resources.rendered) { + const defaultPos = CONFIG.ui.resources.DEFAULT_OPTIONS.position; + await ui.resources.setPosition(defaultPos); + } + ui.notifications.info(game.i18n.localize("COSMOS_DH_TWEAKS.Settings.ResetFearPosition.Success")); + }); + + formFields.innerHTML = ''; + formFields.appendChild(button); + } + } + } +}); diff --git a/styles/cosmos-dh-tweaks.css b/styles/cosmos-dh-tweaks.css index fbcc568..38264d7 100644 --- a/styles/cosmos-dh-tweaks.css +++ b/styles/cosmos-dh-tweaks.css @@ -103,3 +103,77 @@ border: 1px solid var(--golden, #c5a45e) !important; box-shadow: 0 0 5px rgba(197, 164, 94, 0.5) !important; } + +/* Fear Tracker Window Customizations */ +#resources.cosmo-no-title-bar header.window-header { + display: none !important; +} + +#resources.cosmo-no-title-bar { + min-height: 4rem !important; +} + +#resources.cosmo-position-locked .window-resize-handle { + display: none !important; +} + +/* Context menu styling */ +.cosmo-fear-context-menu { + background: rgba(24, 22, 46, 0.95); + backdrop-filter: blur(10px); + border: 2px solid rgb(153, 122, 79); + border-radius: 6px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); + padding: 4px 0; + min-width: 150px; + font-family: inherit; +} + +.cosmo-context-menu-list { + list-style: none; + margin: 0; + padding: 0; +} + +.cosmo-context-menu-item { + padding: 8px 12px; + color: #d3d3d3; + font-size: 13px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: background-color 0.2s, color 0.2s; +} + +.cosmo-context-menu-item:hover { + background-color: rgba(197, 164, 94, 0.2); + color: #ffffff; +} + +.cosmo-context-menu-item i { + width: 16px; + text-align: center; +} + +/* Reset button styling */ +button.cosmo-reset-fear-pos-btn { + background: rgba(197, 164, 94, 0.1); + border: 1px solid rgb(153, 122, 79); + color: #d3d3d3; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-family: inherit; + transition: background-color 0.2s, color 0.2s, border-color 0.2s; +} + +button.cosmo-reset-fear-pos-btn:hover { + background: rgba(197, 164, 94, 0.25); + border-color: #ffffff; + color: #ffffff; +}