working fine

This commit is contained in:
centron\schwoerer
2025-11-21 12:55:34 +01:00
parent 8083003252
commit f59f14f6db

View File

@@ -1,6 +1,6 @@
const MODULE_ID = "gowlers-tracking-ledger"; const MODULE_ID = "gowlers-tracking-ledger";
const MODULE_VERSION = "0.1.23"; const MODULE_VERSION = "0.1.25";
const TRACK_SETTING = "actorSettings"; const TRACK_SETTING = "actorSettings";
const FLAG_SCOPE = "world"; const FLAG_SCOPE = "world";
const MAX_HISTORY_ROWS = 100; const MAX_HISTORY_ROWS = 100;
@@ -80,14 +80,42 @@ const ledgerState = {
lastCombatEndTime: 0, // Timestamp when combat ended lastCombatEndTime: 0, // Timestamp when combat ended
openDialogs: new Map(), // actorId -> dialog instance (for live updates) openDialogs: new Map(), // actorId -> dialog instance (for live updates)
recentMessages: [], // Array of recent message metadata: { message, timestamp, source } recentMessages: [], // Array of recent message metadata: { message, timestamp, source }
historyPageState: new Map(), // actorId -> { [tabId]: { page, pageSize } }
historyTabState: new Map(), // actorId -> active tab id
}; };
function getHistoryPageState(actorId, tabId) {
if (!actorId || !tabId) return { page: 1, pageSize: 10 };
const perActor = ledgerState.historyPageState.get(actorId) ?? {};
return perActor[tabId] ?? { page: 1, pageSize: 10 };
}
function setHistoryPageState(actorId, tabId, page, pageSize) {
if (!actorId || !tabId) return;
const perActor = ledgerState.historyPageState.get(actorId) ?? {};
perActor[tabId] = {
page: Math.max(1, Number.isFinite(page) ? page : 1),
pageSize: pageSize === "all" ? "all" : (Number(pageSize) > 0 ? Number(pageSize) : 10),
};
ledgerState.historyPageState.set(actorId, perActor);
}
function getActiveHistoryTab(actorId, fallback = "hp") {
if (!actorId) return fallback;
return ledgerState.historyTabState.get(actorId) ?? fallback;
}
function setActiveHistoryTab(actorId, tabId) {
if (!actorId || !tabId) return;
ledgerState.historyTabState.set(actorId, tabId);
}
// Global tab switching function for history dialog // Global tab switching function for history dialog
window.switchHistoryTab = function(tabId) { window.switchHistoryTab = function(tabId, el = null) {
console.log("[GowlersTracking] Tab switched to:", tabId); console.log("[GowlersTracking] Tab switched to:", tabId);
// Find the root element using the data-history-root attribute // Find the root element using the data-history-root attribute
const root = document.querySelector('[data-history-root]'); const root = el?.closest?.('[data-history-root]') ?? document.querySelector('[data-history-root]');
if (!root) { if (!root) {
console.warn("[GowlersTracking] History root element not found"); console.warn("[GowlersTracking] History root element not found");
return; return;
@@ -118,6 +146,11 @@ window.switchHistoryTab = function(tabId) {
} else { } else {
console.warn("[GowlersTracking] Tab panel not found for:", tabId); console.warn("[GowlersTracking] Tab panel not found for:", tabId);
} }
const actorId = root?.getAttribute('data-history-root');
if (actorId) {
setActiveHistoryTab(actorId, tabId);
}
}; };
// Global function for history dialog pagination navigation // Global function for history dialog pagination navigation
@@ -125,14 +158,14 @@ window.historyPageNav = function(button, direction) {
const panel = button.closest('[data-history-panel]'); const panel = button.closest('[data-history-panel]');
if (!panel) return; if (!panel) return;
const currentPage = parseInt(panel.getAttribute('data-page') || '1');
const pageInfo = panel.querySelector('[data-page-info]'); const pageInfo = panel.querySelector('[data-page-info]');
if (!pageInfo) return; if (!pageInfo) return;
const pageMatch = pageInfo.textContent.match(/Page (\d+) \/ (\d+)/); const pageMatch = pageInfo.textContent.match(/Page (\d+) \/ (\d+)/);
if (!pageMatch) return; if (!pageMatch) return;
const totalPages = parseInt(pageMatch[2]); const totalPages = parseInt(pageMatch[2]) || 1;
const currentPage = parseInt(panel.getAttribute('data-page') || pageMatch[1] || '1');
let newPage = currentPage; let newPage = currentPage;
if (direction === 'next' && currentPage < totalPages) { if (direction === 'next' && currentPage < totalPages) {
@@ -145,8 +178,18 @@ window.historyPageNav = function(button, direction) {
panel.setAttribute('data-page', newPage); panel.setAttribute('data-page', newPage);
console.log("[GowlersTracking] History pagination - moving to page:", newPage); console.log("[GowlersTracking] History pagination - moving to page:", newPage);
// Rebuild the table with new page
const root = panel.closest('[data-history-root]'); const root = panel.closest('[data-history-root]');
const actorId = root?.getAttribute('data-history-root');
const tabId = panel.getAttribute('data-history-panel');
const pageSizeAttr = panel.getAttribute('data-page-size') || '10';
const pageSize = pageSizeAttr === 'all' ? 'all' : Number(pageSizeAttr) || 10;
if (actorId && tabId) {
setHistoryPageState(actorId, tabId, newPage, pageSize);
refreshOpenDialogs(actorId);
}
// Rebuild the table with new page
if (root) { if (root) {
const app = root.closest('.app, .window-app, .dialog, [role="dialog"]'); const app = root.closest('.app, .window-app, .dialog, [role="dialog"]');
if (app && app.__vue__?.constructor?.name === 'Dialog') { if (app && app.__vue__?.constructor?.name === 'Dialog') {
@@ -167,10 +210,18 @@ window.historyChangePageSize = function(select) {
panel.setAttribute('data-page', '1'); panel.setAttribute('data-page', '1');
panel.setAttribute('data-page-size', pageSize); panel.setAttribute('data-page-size', pageSize);
const root = panel.closest('[data-history-root]');
const actorId = root?.getAttribute('data-history-root');
const tabId = panel.getAttribute('data-history-panel');
const normalizedSize = pageSize === 'all' ? 'all' : (Number(pageSize) > 0 ? Number(pageSize) : 10);
if (actorId && tabId) {
setHistoryPageState(actorId, tabId, 1, normalizedSize);
refreshOpenDialogs(actorId);
}
console.log("[GowlersTracking] History page size changed to:", pageSize); console.log("[GowlersTracking] History page size changed to:", pageSize);
// Re-render dialog // Re-render dialog
const root = panel.closest('[data-history-root]');
if (root) { if (root) {
const app = root.closest('.app, .window-app, .dialog, [role="dialog"]'); const app = root.closest('.app, .window-app, .dialog, [role="dialog"]');
if (app && app.__vue__?.constructor?.name === 'Dialog') { if (app && app.__vue__?.constructor?.name === 'Dialog') {
@@ -193,8 +244,9 @@ function refreshOpenDialogs(actorId) {
const actor = game.actors.get(actorId); const actor = game.actors.get(actorId);
if (!actor) return; if (!actor) return;
// Rebuild content // Rebuild content using the last active tab for this actor
const content = buildHistoryContent(actor, "hp"); const activeTab = getActiveHistoryTab(actorId, "hp");
const content = buildHistoryContent(actor, activeTab);
dialog.data.content = content; dialog.data.content = content;
// Re-render the dialog // Re-render the dialog
@@ -814,18 +866,7 @@ function buildXpBreakdownTooltip(actor, xpEntry) {
let breakdown = []; let breakdown = [];
if (encounter.participants && encounter.participants.length > 0) { if (encounter.participants && encounter.participants.length > 0) {
// Resolve participant UUIDs/IDs to actor names // Resolve participant UUIDs/IDs to actor names
const participantNames = encounter.participants.slice(0, 2).map(p => { const participantNames = encounter.participants.slice(0, 2).map(resolveParticipantName);
// Try to resolve UUID or actor ID to name
if (p.startsWith("Actor.")) {
const participantActor = fromUuidSync(p);
return participantActor?.name || p.slice(0, 8);
} else if (typeof p === "string" && p.length > 20) {
// Likely a UUID - try to resolve
const participantActor = game.actors.get(p);
return participantActor?.name || p.slice(0, 8);
}
return p; // Already a name
});
const participantsStr = participantNames.join(", "); const participantsStr = participantNames.join(", ");
const more = encounter.participants.length > 2 ? ` +${encounter.participants.length - 2} more` : ""; const more = encounter.participants.length > 2 ? ` +${encounter.participants.length - 2} more` : "";
@@ -847,8 +888,9 @@ function buildXpBreakdownTooltip(actor, xpEntry) {
} }
function buildHistoryContent(actor, tabArg) { function buildHistoryContent(actor, tabArg) {
const initialTab = tabArg ?? "hp"; // Explicitly capture the tab parameter const initialTab = tabArg ?? getActiveHistoryTab(actor.id, "hp"); // Explicitly capture the tab parameter
console.log("[GowlersTracking] buildHistoryContent called with initialTab:", initialTab); console.log("[GowlersTracking] buildHistoryContent called with initialTab:", initialTab);
setActiveHistoryTab(actor.id, initialTab);
const canConfigure = game.user?.isGM; const canConfigure = game.user?.isGM;
const configs = [ const configs = [
{ {
@@ -914,7 +956,7 @@ function buildHistoryContent(actor, tabArg) {
.map( .map(
(cfg) => { (cfg) => {
const isActive = cfg.id === initialTab ? "active" : ""; const isActive = cfg.id === initialTab ? "active" : "";
return `<button type="button" class="history-tab-btn item ${isActive}" data-tab-id="${cfg.id}" onclick="window.switchHistoryTab('${cfg.id}')">${cfg.label}</button>`; return `<button type="button" class="history-tab-btn item ${isActive}" data-tab-id="${cfg.id}" onclick="window.switchHistoryTab('${cfg.id}', this)">${cfg.label}</button>`;
} }
) )
.join(""); .join("");
@@ -924,19 +966,25 @@ function buildHistoryContent(actor, tabArg) {
const entries = actor.getFlag(FLAG_SCOPE, cfg.flag) ?? []; const entries = actor.getFlag(FLAG_SCOPE, cfg.flag) ?? [];
const display = cfg.id === initialTab ? "block" : "none"; const display = cfg.id === initialTab ? "block" : "none";
const isActive = cfg.id === initialTab ? "active" : ""; const isActive = cfg.id === initialTab ? "active" : "";
const rowsPerPage = 10; // Default rows per page for history dialog const state = getHistoryPageState(actor.id, cfg.id);
const totalPages = Math.ceil(entries.length / rowsPerPage); let rowsPerPage = state.pageSize === "all" ? "all" : Math.max(1, Number(state.pageSize) || 10);
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" data-page="1" style="display:${display}"> let currentPage = Math.max(1, Number(state.page) || 1);
${renderHistoryTable(entries, cfg.columns, cfg.id, rowsPerPage, 1)} const totalPages = rowsPerPage === "all" ? 1 : Math.max(1, Math.ceil(entries.length / rowsPerPage));
if (currentPage > totalPages) {
currentPage = totalPages;
setHistoryPageState(actor.id, cfg.id, currentPage, rowsPerPage);
}
const selectOptions = [10, 20, 50, "all"]
.map((value) => `<option value="${value}" ${String(value) === String(rowsPerPage) ? "selected" : ""}>${value === "all" ? "All" : `${value} rows`}</option>`)
.join("");
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" data-page="${currentPage}" data-page-size="${rowsPerPage}" style="display:${display}">
${renderHistoryTable(entries, cfg.columns, cfg.id, rowsPerPage, currentPage)}
<div class="history-panel-pagination" style="display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 12px; padding-top: 8px; border-top: 1px solid #e0e0e0;"> <div class="history-panel-pagination" style="display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 12px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
<button type="button" data-action="history-page" data-direction="prev" onclick="window.historyPageNav(this, 'prev')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>« Prev</button> <button type="button" data-action="history-page" data-direction="prev" onclick="window.historyPageNav(this, 'prev')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>« Prev</button>
<span data-page-info style="font-size: 0.85em; min-width: 100px; text-align: center;">Page 1 / ${Math.max(1, totalPages)}</span> <span data-page-info style="font-size: 0.85em; min-width: 100px; text-align: center;">Page ${currentPage} / ${Math.max(1, totalPages)}</span>
<button type="button" data-action="history-page" data-direction="next" onclick="window.historyPageNav(this, 'next')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>Next »</button> <button type="button" data-action="history-page" data-direction="next" onclick="window.historyPageNav(this, 'next')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>Next »</button>
<select data-history-page-size onchange="window.historyChangePageSize(this)" style="padding: 2px 4px;"> <select data-history-page-size onchange="window.historyChangePageSize(this)" style="padding: 2px 4px;">
<option value="10" selected>10 rows</option> ${selectOptions}
<option value="20">20 rows</option>
<option value="50">50 rows</option>
<option value="all">All</option>
</select> </select>
</div> </div>
</div>`; </div>`;
@@ -1030,8 +1078,21 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
let encounterId = game.combat?.id ?? null; let encounterId = game.combat?.id ?? null;
console.log(`[GowlersTracking] Recording ${statId} change - Active combat: ${encounterId ? "Yes (" + encounterId + ")" : "No"}`); console.log(`[GowlersTracking] Recording ${statId} change - Active combat: ${encounterId ? "Yes (" + encounterId + ")" : "No"}`);
// If no active combat but XP is being recorded shortly after combat ended, link to last encounter // If no active combat but XP is being recorded shortly after combat ended, link to last encounter (unless explicitly manual)
if (!encounterId && statId === "xp" && ledgerState.lastCombatId) { const manualXp =
statId === "xp" &&
(options?.manualXp === true || options?.type === "ManualXP" || options?.skipEncounterLink === true);
const hasExplicitEncounter =
statId === "xp" &&
(options?.encounterId || change?.encounterId || options?.encounter?.id || options?.encounter?._id);
const canLinkXpToLastCombat =
statId === "xp" &&
!manualXp &&
(options?.type === "EncounterXP" || options?.type === "Encounter" || hasExplicitEncounter || game.combat?.id || ledgerState.lastCombatId);
if (!encounterId && statId === "xp" && ledgerState.lastCombatId && canLinkXpToLastCombat) {
const timeSinceEnd = Date.now() - ledgerState.lastCombatEndTime; const timeSinceEnd = Date.now() - ledgerState.lastCombatEndTime;
// Link to last encounter if within 30 seconds (allows time for XP award after combat ends) // Link to last encounter if within 30 seconds (allows time for XP award after combat ends)
if (timeSinceEnd < 30000) { if (timeSinceEnd < 30000) {
@@ -1042,6 +1103,14 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
} }
} }
// Allow explicit encounter ID/name payloads to override when provided (e.g., encounter XP awards)
if (statId === "xp") {
if (options?.encounterId) encounterId = options.encounterId;
else if (change?.encounterId) encounterId = change.encounterId;
else if (options?.encounter?.id) encounterId = options.encounter.id;
else if (options?.encounter?._id) encounterId = options.encounter._id;
}
// Detect source of the change with actor and item details // Detect source of the change with actor and item details
let source = "Manual"; let source = "Manual";
let sourceDetails = ""; let sourceDetails = "";
@@ -1091,8 +1160,10 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
damageBreakdown = `${hpDiff} healing`; damageBreakdown = `${hpDiff} healing`;
} }
} else if (statId === "xp" && diffValue > 0) { } else if (statId === "xp" && diffValue > 0) {
// XP gains - could be from encounter end or manual award // XP gains - encounter or manual award
source = "XP Award"; source = options?.source ?? (encounterId ? "Encounter XP Award" : "XP Award");
if (options?.encounterName) sourceDetails = options.encounterName;
if (!sourceDetails && encounterId) sourceDetails = encounterId.slice?.(0, 8) ?? encounterId;
damageBreakdown = `${diffValue} XP`; damageBreakdown = `${diffValue} XP`;
} else if (statId === "currency" && diffValue !== 0) { } else if (statId === "currency" && diffValue !== 0) {
// Currency changes // Currency changes
@@ -1286,6 +1357,41 @@ function isNonlethalType(type) {
return String(type ?? "").toLowerCase() === "nonlethal"; return String(type ?? "").toLowerCase() === "nonlethal";
} }
function resolveParticipantName(participant) {
if (!participant) return "Unknown";
// Objects that already include an id or name
if (typeof participant === "object") {
if (participant.name) return participant.name;
if (participant.id) {
const actor = game.actors.get(participant.id);
if (actor) return actor.name;
}
}
if (typeof participant === "string") {
// Try UUID first (token or actor)
try {
if (typeof fromUuidSync === "function") {
const doc = fromUuidSync(participant);
if (doc?.name) return doc.name;
if (doc?.actor?.name) return doc.actor.name;
}
} catch (err) {
// Ignore UUID errors, fall through
}
// Try direct actor lookup by ID
const actor = game.actors.get(participant);
if (actor) return actor.name;
// Fallback to trimmed ID preview
return participant.slice(0, 8);
}
return "Unknown";
}
function checkboxValue(value, fallback = false) { function checkboxValue(value, fallback = false) {
if (value === undefined || value === null) return fallback; if (value === undefined || value === null) return fallback;
if (Array.isArray(value)) value = value[value.length - 1]; if (Array.isArray(value)) value = value[value.length - 1];