diff --git a/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js b/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js index d3d35072..79a3e8a7 100644 --- a/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js +++ b/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js @@ -12,6 +12,7 @@ const DEFAULT_TRACKING = Object.freeze({ hp: true, xp: true, currency: true }); const DEFAULT_CHAT = Object.freeze({ hp: false, xp: false, currency: false }); const SETTINGS_VERSION = 2; const COIN_ORDER = ["pp", "gp", "sp", "cp"]; +const ENCOUNTER_FLAG = "pf1EncounterHistory"; const STAT_CONFIGS = { hp: { @@ -103,6 +104,11 @@ async function initializeModule() { }) ); + // Track combat encounters + Hooks.on("combatStart", (combat) => onCombatStart(combat)); + Hooks.on("combatEnd", (combat) => onCombatEnd(combat)); + Hooks.on("updateCombat", (combat) => onCombatUpdate(combat)); + const api = { initialized: true, openConfig: () => new TrackingLedgerConfig().render(true), @@ -284,7 +290,9 @@ function findHeaderByText(root, text) { function openHistoryDialog(actor, initialTab = "hp") { if (!actor) return; const content = buildHistoryContent(actor, initialTab); - new Dialog( + + // Create a custom dialog with header button + const dialog = new Dialog( { title: `${actor.name}: Log`, content, @@ -293,8 +301,107 @@ function openHistoryDialog(actor, initialTab = "hp") { { width: 720, classes: ["pf1-history-dialog"], + render: (html) => { + // html is jQuery object of the dialog element + // Find the content root within the dialog + const root = html.find('[data-history-root]')[0]; + if (!root) { + console.warn("[History Dialog] Root element not found"); + return; + } + + console.log("[History Dialog] Root element found, setting up tabs"); + + // Get all tab buttons and panels + const buttons = Array.from(root.querySelectorAll('.history-tab-link')); + const panels = Array.from(root.querySelectorAll('.history-panel')); + + console.log(`[History Dialog] Found ${buttons.length} tabs and ${panels.length} panels`); + + // Tab activation function + const activateTab = (tabId) => { + console.log(`[History Dialog] Activating tab: ${tabId}`); + buttons.forEach((btn) => { + const shouldBeActive = btn.dataset.historyTab === tabId; + btn.classList.toggle('active', shouldBeActive); + }); + panels.forEach((panel) => { + const shouldBeActive = panel.dataset.historyPanel === tabId; + panel.style.display = shouldBeActive ? 'block' : 'none'; + panel.classList.toggle('active', shouldBeActive); + }); + }; + + // Bind tab click handlers using event delegation + buttons.forEach((btn, index) => { + btn.style.cursor = 'pointer'; + btn.addEventListener('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + const tabId = this.dataset.historyTab; + console.log(`[History Dialog] Tab clicked: ${tabId}`); + activateTab(tabId); + return false; + }); + }); + + // Bind config button in content + const configBtn = root.querySelector('[data-action="open-config"]'); + if (configBtn) { + configBtn.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log("[History Dialog] Config button clicked (content)"); + const api = window.GowlersTrackingLedger; + if (api?.openConfigForActor) { + api.openConfigForActor(actor.id); + } else if (api?.openConfig) { + api.openConfig(); + } + }); + } + + // Add config button to dialog header if user is GM + if (game.user?.isGM) { + const header = html.find('.dialog-header')[0]; + if (header) { + const existingBtn = header.querySelector('[data-history-config-header]'); + if (!existingBtn) { + const configHeaderBtn = document.createElement('button'); + configHeaderBtn.type = 'button'; + configHeaderBtn.className = 'history-config-header-btn'; + configHeaderBtn.setAttribute('data-history-config-header', 'true'); + configHeaderBtn.setAttribute('title', 'Configure Actor Tracking'); + configHeaderBtn.innerHTML = ''; + configHeaderBtn.style.cssText = 'border: none; background: transparent; color: #666; cursor: pointer; padding: 8px 10px; margin-right: 8px; transition: color 0.2s;'; + configHeaderBtn.addEventListener('mouseover', () => configHeaderBtn.style.color = '#333'); + configHeaderBtn.addEventListener('mouseout', () => configHeaderBtn.style.color = '#666'); + configHeaderBtn.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log("[History Dialog] Config button clicked (header)"); + const api = window.GowlersTrackingLedger; + if (api?.openConfigForActor) { + api.openConfigForActor(actor.id); + } else if (api?.openConfig) { + api.openConfig(); + } + }); + const closeBtn = header.querySelector('.close'); + if (closeBtn) { + header.insertBefore(configHeaderBtn, closeBtn); + } else { + header.appendChild(configHeaderBtn); + } + console.log("[History Dialog] Config header button added"); + } + } + } + }, } - ).render(true); + ); + + dialog.render(true); } function buildHistoryContent(actor, initialTab = "hp") { @@ -309,7 +416,7 @@ function buildHistoryContent(actor, initialTab = "hp") { { label: "HP", render: (entry) => entry.value }, { label: "Δ", render: (entry) => entry.diff }, { label: "User", render: (entry) => entry.user ?? "" }, - { label: "Source", render: (entry) => entry.source ?? "" }, + { label: "Encounter", render: (entry) => entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A" }, ], }, { @@ -321,7 +428,7 @@ function buildHistoryContent(actor, initialTab = "hp") { { label: "XP", render: (entry) => entry.value }, { label: "Δ", render: (entry) => entry.diff }, { label: "User", render: (entry) => entry.user ?? "" }, - { label: "Source", render: (entry) => entry.source ?? "" }, + { label: "Encounter", render: (entry) => entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A" }, ], }, { @@ -333,7 +440,19 @@ function buildHistoryContent(actor, initialTab = "hp") { { label: "Totals", render: (entry) => entry.value }, { label: "Δ", render: (entry) => entry.diff }, { label: "User", render: (entry) => entry.user ?? "" }, - { label: "Source", render: (entry) => entry.source ?? "" }, + { label: "Encounter", render: (entry) => entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A" }, + ], + }, + { + id: "encounters", + label: "Encounters", + flag: ENCOUNTER_FLAG, + columns: [ + { label: "Encounter ID", render: (entry) => entry.encounterID.slice(0, 8) }, + { label: "Date", render: (entry) => formatDate(entry.dateCreated) }, + { label: "Status", render: (entry) => entry.status ?? "unknown" }, + { label: "Rounds", render: (entry) => entry.rounds || 0 }, + { label: "Participants", render: (entry) => entry.participants ? entry.participants.length : 0 }, ], }, ]; @@ -382,33 +501,6 @@ function buildHistoryContent(actor, initialTab = "hp") { ${toolbar}
${panels}
- `; } @@ -448,6 +540,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId) { diff: config.formatDiff(diffValue), user: game.users.get(userId)?.name ?? "System", source: "", + encounterId: game.combat?.id ?? null, // Track which encounter this change occurred in }; const existing = (await actor.getFlag(FLAG_SCOPE, config.flag)) ?? []; @@ -641,11 +734,11 @@ class TrackingLedgerConfig extends FormApplication { }); } - static PAGE_OPTIONS = [25, 50, 100, 250]; - static DEFAULT_PAGE_SIZE = 50; + static PAGE_OPTIONS = [10, 20, "all"]; + static DEFAULT_PAGE_SIZE = 10; static _lastFilter = ""; static _lastPage = 0; - static _lastPageSize = 50; + static _lastPageSize = 10; constructor(...args) { super(...args); @@ -654,10 +747,12 @@ class TrackingLedgerConfig extends FormApplication { this._pageSize = TrackingLedgerConfig._lastPageSize ?? TrackingLedgerConfig.DEFAULT_PAGE_SIZE; this._pageMeta = { totalPages: 1, hasPrev: false, hasNext: false }; this._actorRefs = null; + this._filterDebounceTimer = null; } get pageSize() { - return this._pageSize ?? TrackingLedgerConfig.DEFAULT_PAGE_SIZE; + const size = this._pageSize ?? TrackingLedgerConfig.DEFAULT_PAGE_SIZE; + return size === "all" ? Infinity : (Number.isFinite(size) ? size : TrackingLedgerConfig.DEFAULT_PAGE_SIZE); } async getData() { @@ -771,17 +866,30 @@ class TrackingLedgerConfig extends FormApplication { activateListeners(html) { super.activateListeners(html); - html.find("[data-filter-input]").on("input", (event) => { + const filterInput = html.find("[data-filter-input]"); + filterInput.on("input", (event) => { this._filter = event.currentTarget.value ?? ""; this._page = 0; TrackingLedgerConfig._lastFilter = this._filter; TrackingLedgerConfig._lastPage = this._page; - this.render(false); + + // Debounce render to preserve focus + clearTimeout(this._filterDebounceTimer); + this._filterDebounceTimer = setTimeout(() => { + this.render(false); + }, 300); }); + // Keep focus on filter input after render + filterInput.trigger("focus"); html.find("[data-page-size]").on("change", (event) => { - const value = Number(event.currentTarget.value); - this._pageSize = Number.isFinite(value) && value > 0 ? value : TrackingLedgerConfig.DEFAULT_PAGE_SIZE; + const value = event.currentTarget.value; + if (value === "all") { + this._pageSize = "all"; + } else { + const num = Number(value); + this._pageSize = Number.isFinite(num) && num > 0 ? num : TrackingLedgerConfig.DEFAULT_PAGE_SIZE; + } TrackingLedgerConfig._lastPageSize = this._pageSize; this._page = 0; TrackingLedgerConfig._lastPage = 0; @@ -814,6 +922,87 @@ class TrackingLedgerConfig extends FormApplication { } } +/** + * Update encounter summary when combat starts + */ +async function onCombatStart(combat) { + if (!combat) return; + for (const combatant of combat.combatants) { + const actor = combatant.actor; + if (!actor) continue; + await updateEncounterSummary(actor, combat, "ongoing"); + } +} + +/** + * Update encounter summary when combat round changes or turns change + */ +async function onCombatUpdate(combat) { + if (!combat) return; + for (const combatant of combat.combatants) { + const actor = combatant.actor; + if (!actor) continue; + await updateEncounterSummary(actor, combat, "ongoing"); + } +} + +/** + * Finalize encounter summary when combat ends + */ +async function onCombatEnd(combat) { + if (!combat) return; + for (const combatant of combat.combatants) { + const actor = combatant.actor; + if (!actor) continue; + await updateEncounterSummary(actor, combat, "ended"); + } +} + +/** + * Update or create encounter summary for an actor + */ +async function updateEncounterSummary(actor, combat, status = "ongoing") { + if (!actor || !combat) return; + + const existing = (await actor.getFlag(FLAG_SCOPE, ENCOUNTER_FLAG)) ?? []; + + // Find existing entry for this combat + let encEntry = existing.find((entry) => entry.encounterID === combat.id); + + if (!encEntry) { + encEntry = { + encounterID: combat.id, + dateCreated: Date.now(), + dateUpdated: Date.now(), + participants: [], + status: status, + xpGained: 0, + rounds: 0, + }; + existing.unshift(encEntry); + } else { + encEntry.dateUpdated = Date.now(); + encEntry.status = status; + encEntry.rounds = combat.round || 0; + } + + // Update participant list with all combatants + const participantIds = new Set(); + for (const combatant of combat.combatants) { + if (combatant.actor) { + participantIds.add(combatant.actor.id); + } + } + encEntry.participants = Array.from(participantIds); + + // Keep recent encounters (max 50) + if (existing.length > 50) { + existing.splice(50); + } + + await actor.setFlag(FLAG_SCOPE, ENCOUNTER_FLAG, existing); +} + function sendChatNotification(statId, actor, previous, nextValue, entry) { const config = STAT_CONFIGS[statId]; if (!config) return; diff --git a/src/macros_new/gowlers-tracking-ledger/templates/config.hbs b/src/macros_new/gowlers-tracking-ledger/templates/config.hbs index 658cc1da..00d5a2cb 100644 --- a/src/macros_new/gowlers-tracking-ledger/templates/config.hbs +++ b/src/macros_new/gowlers-tracking-ledger/templates/config.hbs @@ -1,27 +1,29 @@
- - -
- - {{#if totalActors}} - Page {{displayPage}} / {{totalPages}} - {{else}} - Page 0 / 0 - {{/if}} - +
+ + +
+ + {{#if totalActors}} + Page {{displayPage}} / {{totalPages}} + {{else}} + Page 0 / 0 + {{/if}} + +
-
+
{{#if totalActors}} Showing {{showingFrom}}-{{showingTo}} of {{totalActors}} actors {{else}}