working fine
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
|
||||
const MODULE_ID = "gowlers-tracking-ledger";
|
||||
const MODULE_VERSION = "0.1.23";
|
||||
const MODULE_VERSION = "0.1.25";
|
||||
const TRACK_SETTING = "actorSettings";
|
||||
const FLAG_SCOPE = "world";
|
||||
const MAX_HISTORY_ROWS = 100;
|
||||
@@ -80,14 +80,42 @@ const ledgerState = {
|
||||
lastCombatEndTime: 0, // Timestamp when combat ended
|
||||
openDialogs: new Map(), // actorId -> dialog instance (for live updates)
|
||||
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
|
||||
window.switchHistoryTab = function(tabId) {
|
||||
window.switchHistoryTab = function(tabId, el = null) {
|
||||
console.log("[GowlersTracking] Tab switched to:", tabId);
|
||||
|
||||
// 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) {
|
||||
console.warn("[GowlersTracking] History root element not found");
|
||||
return;
|
||||
@@ -118,6 +146,11 @@ window.switchHistoryTab = function(tabId) {
|
||||
} else {
|
||||
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
|
||||
@@ -125,14 +158,14 @@ window.historyPageNav = function(button, direction) {
|
||||
const panel = button.closest('[data-history-panel]');
|
||||
if (!panel) return;
|
||||
|
||||
const currentPage = parseInt(panel.getAttribute('data-page') || '1');
|
||||
const pageInfo = panel.querySelector('[data-page-info]');
|
||||
if (!pageInfo) return;
|
||||
|
||||
const pageMatch = pageInfo.textContent.match(/Page (\d+) \/ (\d+)/);
|
||||
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;
|
||||
|
||||
if (direction === 'next' && currentPage < totalPages) {
|
||||
@@ -145,8 +178,18 @@ window.historyPageNav = function(button, direction) {
|
||||
panel.setAttribute('data-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 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) {
|
||||
const app = root.closest('.app, .window-app, .dialog, [role="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-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);
|
||||
|
||||
// Re-render dialog
|
||||
const root = panel.closest('[data-history-root]');
|
||||
if (root) {
|
||||
const app = root.closest('.app, .window-app, .dialog, [role="dialog"]');
|
||||
if (app && app.__vue__?.constructor?.name === 'Dialog') {
|
||||
@@ -193,8 +244,9 @@ function refreshOpenDialogs(actorId) {
|
||||
const actor = game.actors.get(actorId);
|
||||
if (!actor) return;
|
||||
|
||||
// Rebuild content
|
||||
const content = buildHistoryContent(actor, "hp");
|
||||
// Rebuild content using the last active tab for this actor
|
||||
const activeTab = getActiveHistoryTab(actorId, "hp");
|
||||
const content = buildHistoryContent(actor, activeTab);
|
||||
dialog.data.content = content;
|
||||
|
||||
// Re-render the dialog
|
||||
@@ -814,18 +866,7 @@ function buildXpBreakdownTooltip(actor, xpEntry) {
|
||||
let breakdown = [];
|
||||
if (encounter.participants && encounter.participants.length > 0) {
|
||||
// Resolve participant UUIDs/IDs to actor names
|
||||
const participantNames = encounter.participants.slice(0, 2).map(p => {
|
||||
// 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 participantNames = encounter.participants.slice(0, 2).map(resolveParticipantName);
|
||||
|
||||
const participantsStr = participantNames.join(", ");
|
||||
const more = encounter.participants.length > 2 ? ` +${encounter.participants.length - 2} more` : "";
|
||||
@@ -847,8 +888,9 @@ function buildXpBreakdownTooltip(actor, xpEntry) {
|
||||
}
|
||||
|
||||
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);
|
||||
setActiveHistoryTab(actor.id, initialTab);
|
||||
const canConfigure = game.user?.isGM;
|
||||
const configs = [
|
||||
{
|
||||
@@ -914,7 +956,7 @@ function buildHistoryContent(actor, tabArg) {
|
||||
.map(
|
||||
(cfg) => {
|
||||
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("");
|
||||
@@ -924,19 +966,25 @@ function buildHistoryContent(actor, tabArg) {
|
||||
const entries = actor.getFlag(FLAG_SCOPE, cfg.flag) ?? [];
|
||||
const display = cfg.id === initialTab ? "block" : "none";
|
||||
const isActive = cfg.id === initialTab ? "active" : "";
|
||||
const rowsPerPage = 10; // Default rows per page for history dialog
|
||||
const totalPages = Math.ceil(entries.length / rowsPerPage);
|
||||
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" data-page="1" style="display:${display}">
|
||||
${renderHistoryTable(entries, cfg.columns, cfg.id, rowsPerPage, 1)}
|
||||
const state = getHistoryPageState(actor.id, cfg.id);
|
||||
let rowsPerPage = state.pageSize === "all" ? "all" : Math.max(1, Number(state.pageSize) || 10);
|
||||
let currentPage = Math.max(1, Number(state.page) || 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;">
|
||||
<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>
|
||||
<select data-history-page-size onchange="window.historyChangePageSize(this)" style="padding: 2px 4px;">
|
||||
<option value="10" selected>10 rows</option>
|
||||
<option value="20">20 rows</option>
|
||||
<option value="50">50 rows</option>
|
||||
<option value="all">All</option>
|
||||
${selectOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -1030,8 +1078,21 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
||||
let encounterId = game.combat?.id ?? null;
|
||||
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 (!encounterId && statId === "xp" && ledgerState.lastCombatId) {
|
||||
// If no active combat but XP is being recorded shortly after combat ended, link to last encounter (unless explicitly manual)
|
||||
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;
|
||||
// Link to last encounter if within 30 seconds (allows time for XP award after combat ends)
|
||||
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
|
||||
let source = "Manual";
|
||||
let sourceDetails = "";
|
||||
@@ -1091,8 +1160,10 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
||||
damageBreakdown = `${hpDiff} healing`;
|
||||
}
|
||||
} else if (statId === "xp" && diffValue > 0) {
|
||||
// XP gains - could be from encounter end or manual award
|
||||
source = "XP Award";
|
||||
// XP gains - encounter or manual 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`;
|
||||
} else if (statId === "currency" && diffValue !== 0) {
|
||||
// Currency changes
|
||||
@@ -1286,6 +1357,41 @@ function isNonlethalType(type) {
|
||||
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) {
|
||||
if (value === undefined || value === null) return fallback;
|
||||
if (Array.isArray(value)) value = value[value.length - 1];
|
||||
|
||||
Reference in New Issue
Block a user