working fine
This commit is contained in:
@@ -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];
|
||||||
|
|||||||
Reference in New Issue
Block a user