add fear tracker options

This commit is contained in:
CPTN Cosmo 2026-07-12 02:12:09 +02:00
parent e020a4efd7
commit 7a20b05c76
3 changed files with 425 additions and 11 deletions

View file

@ -9,5 +9,13 @@
"COSMOS_DH_TWEAKS.Spotlight.cancel": "Cancel Spotlight Request", "COSMOS_DH_TWEAKS.Spotlight.cancel": "Cancel Spotlight Request",
"COSMOS_DH_TWEAKS.Spotlight.grant": "Grant Spotlight", "COSMOS_DH_TWEAKS.Spotlight.grant": "Grant Spotlight",
"COSMOS_DH_TWEAKS.Spotlight.take": "Take 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"
} }

View file

@ -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
? `<i class="fa-solid fa-lock-open"></i> ${game.i18n.localize("COSMOS_DH_TWEAKS.ContextMenu.UnlockPosition")}`
: `<i class="fa-solid fa-lock"></i> ${game.i18n.localize("COSMOS_DH_TWEAKS.ContextMenu.LockPosition")}`;
menu.innerHTML = `
<ul class="cosmo-context-menu-list">
<li class="cosmo-context-menu-item lock-toggle" data-action="toggle-lock">
${lockText}
</li>
</ul>
`;
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 // Override the Countdown class in CONFIG
const OriginalCountdowns = CONFIG.ui.countdowns; const OriginalCountdowns = CONFIG.ui.countdowns;
if (OriginalCountdowns) { if (OriginalCountdowns) {
@ -295,16 +592,20 @@ Hooks.on("refreshToken", (token, options) => {
bg.endFill(); bg.endFill();
container.addChild(bg); container.addChild(bg);
// Draw FontAwesome solid e05d hand sparkles icon // FontAwesome hand-sparkles SVG data URI
const style = new PIXI.TextStyle({ const handSparklesSvg = `
fontFamily: '"Font Awesome 6 Free"', <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="640" height="512">
fontWeight: "900", <path fill="#ffffff" d="M320 0c17.7 0 32 14.3 32 32l0 208c0 8.8 7.2 16 16 16s16-7.2 16-16l0-176c0-17.7 14.3-32 32-32s32 14.3 32 32l0 176c0 8.8 7.2 16 16 16s16-7.2 16-16l0-112c0-17.7 14.3-32 32-32s32 14.3 32 32l0 178.2c-19.2 5.4-34.7 20.4-40.4 40.3l-6.5 22.7-22.7 6.5c-25.2 7.2-42.5 30.2-42.5 56.4 0 22.1 12.4 42 31.4 51.9-27.5 17.8-60.2 28.1-95.4 28.1l-19.2 0c-59.6 0-116.9-22.9-160-64L76.4 341c-16-15.2-16.6-40.6-1.4-56.6s40.6-16.6 56.6-1.4l60.5 57.6c0-1.5-.1-3.1-.1-4.6l0-272c0-17.7 14.3-32 32-32s32 14.3 32 32l0 176c0 8.8 7.2 16 16 16s16-7.2 16-16l0-208c0-17.7 14.3-32 32-32zm-7.3 326.6c-1.1-3.9-4.7-6.6-8.7-6.6s-7.6 2.7-8.7 6.6l-7.3 25.4-25.4 7.3c-3.9 1.1-6.6 4.7-6.6 8.7s2.7 7.6 6.6 8.7l25.4 7.3 7.3 25.4c1.1 3.9 4.7 6.6 8.7 6.6s7.6-2.7 8.7-6.6l7.3-25.4 25.4-7.3c3.9-1.1 6.6-4.7 6.6-8.7s-2.7-7.6-6.6-8.7l-25.4-7.3-7.3-25.4zM104 120l48.3 13.8c4.6 1.3 7.7 5.5 7.7 10.2s-3.1 8.9-7.7 10.2L104 168 90.2 216.3c-1.3 4.6-5.5 7.7-10.2 7.7s-8.9-3.1-10.2-7.7L56 168 7.7 154.2C3.1 152.9 0 148.7 0 144s3.1-8.9 7.7-10.2L56 120 69.8 71.7C71.1 67.1 75.3 64 80 64s8.9 3.1 10.2 7.7L104 120zM584 408l48.3 13.8c4.6 1.3 7.7 5.5 7.7 10.2s-3.1 8.9-7.7 10.2L584 456 570.2 504.3c-1.3 4.6-5.5 7.7-10.2 7.7s-8.9-3.1-10.2-7.7L536 456 487.7 442.2c-4.6-1.3-7.7-5.5-7.7-10.2s3.1-8.9 7.7-10.2L536 408 549.8 359.7c1.3-4.6 5.5-7.7 10.2-7.7s8.9 3.1 10.2 7.7L584 408z"/>
fontSize: 13, </svg>
fill: "#ffffff" `;
}); const svgDataUri = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(handSparklesSvg)));
const text = new PIXI.Text("\ue05d", style); const handTexture = PIXI.Texture.from(svgDataUri);
text.anchor.set(0.5); const handSprite = new PIXI.Sprite(handTexture);
container.addChild(text); 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); token.cosmoSpotlightIndicator = token.addChild(container);
} }
@ -325,3 +626,34 @@ Hooks.on("destroyToken", (token) => {
token.cosmoSpotlightIndicator = null; 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 = `<i class="fa-solid fa-arrows-rotate"></i> ${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);
}
}
}
});

View file

@ -103,3 +103,77 @@
border: 1px solid var(--golden, #c5a45e) !important; border: 1px solid var(--golden, #c5a45e) !important;
box-shadow: 0 0 5px rgba(197, 164, 94, 0.5) !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;
}