fix(gowlers-tracking-ledger): preserve value sign for correct damage classification
- Fix critical bug where damage was classified as 'Healing' instead of 'Damage' - Changed queue storage to preserve sign of value (negative=damage, positive=healing) - This allows buildSourceLabel() to correctly classify HP changes as damage vs healing - Update version to 0.1.18 The issue was storing Math.abs(value) which stripped the sign. Now storing raw value so that damage classification logic can use: value < 0 ? "Damage" : "Healing" 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
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.13";
|
const MODULE_VERSION = "0.1.18";
|
||||||
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;
|
||||||
@@ -78,6 +78,8 @@ const ledgerState = {
|
|||||||
actorSettings: null,
|
actorSettings: null,
|
||||||
lastCombatId: null, // Track last combat ID to link XP gains after combat ends
|
lastCombatId: null, // Track last combat ID to link XP gains after combat ends
|
||||||
lastCombatEndTime: 0, // Timestamp when combat ended
|
lastCombatEndTime: 0, // Timestamp when combat ended
|
||||||
|
openDialogs: new Map(), // actorId -> dialog instance (for live updates)
|
||||||
|
recentMessages: [], // Array of recent message metadata: { message, timestamp, source }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global tab switching function for history dialog
|
// Global tab switching function for history dialog
|
||||||
@@ -118,6 +120,90 @@ window.switchHistoryTab = function(tabId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Global function for history dialog pagination navigation
|
||||||
|
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]);
|
||||||
|
let newPage = currentPage;
|
||||||
|
|
||||||
|
if (direction === 'next' && currentPage < totalPages) {
|
||||||
|
newPage = currentPage + 1;
|
||||||
|
} else if (direction === 'prev' && currentPage > 1) {
|
||||||
|
newPage = currentPage - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPage !== currentPage) {
|
||||||
|
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]');
|
||||||
|
if (root) {
|
||||||
|
const app = root.closest('.app, .window-app, .dialog, [role="dialog"]');
|
||||||
|
if (app && app.__vue__?.constructor?.name === 'Dialog') {
|
||||||
|
// Re-render the dialog to show new page
|
||||||
|
app.__vue__.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function for history dialog page size changes
|
||||||
|
window.historyChangePageSize = function(select) {
|
||||||
|
const pageSize = select.value;
|
||||||
|
const panel = select.closest('[data-history-panel]');
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
// Reset to page 1 when changing page size
|
||||||
|
panel.setAttribute('data-page', '1');
|
||||||
|
panel.setAttribute('data-page-size', pageSize);
|
||||||
|
|
||||||
|
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') {
|
||||||
|
app.__vue__.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to refresh open dialogs with new data
|
||||||
|
function refreshOpenDialogs(actorId) {
|
||||||
|
const dialog = ledgerState.openDialogs.get(actorId);
|
||||||
|
if (!dialog || !dialog._state || !dialog.rendered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[GowlersTracking] Refreshing dialog for actor:", actorId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get actor
|
||||||
|
const actor = game.actors.get(actorId);
|
||||||
|
if (!actor) return;
|
||||||
|
|
||||||
|
// Rebuild content
|
||||||
|
const content = buildHistoryContent(actor, "hp");
|
||||||
|
dialog.data.content = content;
|
||||||
|
|
||||||
|
// Re-render the dialog
|
||||||
|
dialog.render(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[GowlersTracking] Error refreshing dialog:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
if (game.system.id !== "pf1") return;
|
if (game.system.id !== "pf1") return;
|
||||||
|
|
||||||
@@ -133,6 +219,52 @@ Hooks.once("ready", async () => {
|
|||||||
async function initializeModule() {
|
async function initializeModule() {
|
||||||
if (globalThis.GowlersTrackingLedger?.initialized) return;
|
if (globalThis.GowlersTrackingLedger?.initialized) return;
|
||||||
|
|
||||||
|
// Install ActorPF.applyDamage wrapper NOW that system is ready
|
||||||
|
console.log("[GowlersTracking] Installing ActorPF.applyDamage wrapper...");
|
||||||
|
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
||||||
|
console.log("[GowlersTracking] ActorPF lookup:", ActorPF ? "Found" : "Not found", "| Already wrapped:", ActorPF?._trackedByGowlers);
|
||||||
|
if (ActorPF && !ActorPF._trackedByGowlers) {
|
||||||
|
// Wrap the STATIC method (not the instance method!)
|
||||||
|
// PF1 system calls ActorPF.applyDamage() directly from chat cards
|
||||||
|
const original = ActorPF.applyDamage;
|
||||||
|
console.log("[GowlersTracking] Original static applyDamage:", original ? "Found" : "Not found");
|
||||||
|
|
||||||
|
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
||||||
|
console.log("[GowlersTracking] STATIC applyDamage called! value:", value, "options keys:", Object.keys(options));
|
||||||
|
|
||||||
|
// Store message metadata for later matching in updateActor
|
||||||
|
try {
|
||||||
|
const message = options.message;
|
||||||
|
if (message) {
|
||||||
|
const source = buildSourceLabel(value, options);
|
||||||
|
console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", value);
|
||||||
|
|
||||||
|
ledgerState.recentMessages.push({
|
||||||
|
message: message,
|
||||||
|
source: source,
|
||||||
|
value: value, // Keep sign! Negative = damage, Positive = healing
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 50 messages to prevent memory leak
|
||||||
|
if (ledgerState.recentMessages.length > 50) {
|
||||||
|
ledgerState.recentMessages.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[GowlersTracking] Failed to store message metadata", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original static method
|
||||||
|
return original.call(this, value, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActorPF._trackedByGowlers = true;
|
||||||
|
console.log("[GowlersTracking] ActorPF.applyDamage STATIC wrapper installed successfully");
|
||||||
|
} else {
|
||||||
|
console.log("[GowlersTracking] ActorPF wrapper skipped - ActorPF found:", !!ActorPF, "Already wrapped:", ActorPF?._trackedByGowlers);
|
||||||
|
}
|
||||||
|
|
||||||
await primeAllActors();
|
await primeAllActors();
|
||||||
ledgerState.updateHook = Hooks.on("updateActor", handleActorUpdate);
|
ledgerState.updateHook = Hooks.on("updateActor", handleActorUpdate);
|
||||||
ledgerState.createHook = Hooks.on("createActor", handleCreateActor);
|
ledgerState.createHook = Hooks.on("createActor", handleCreateActor);
|
||||||
@@ -150,6 +282,92 @@ async function initializeModule() {
|
|||||||
Hooks.on("deleteCombat", (combat) => onCombatEnd(combat));
|
Hooks.on("deleteCombat", (combat) => onCombatEnd(combat));
|
||||||
Hooks.on("updateCombat", (combat) => onCombatUpdate(combat));
|
Hooks.on("updateCombat", (combat) => onCombatUpdate(combat));
|
||||||
|
|
||||||
|
// Helper: Build source label from damage/healing context
|
||||||
|
function buildSourceLabel(value, options) {
|
||||||
|
// Order of precedence (per Manual_dmgtracking.md):
|
||||||
|
// 1. identifiedInfo from chat message
|
||||||
|
// 2. Chat card metadata (actor/item)
|
||||||
|
// 3. Chat card flavor text
|
||||||
|
// 4. Fallbacks
|
||||||
|
|
||||||
|
const message = options?.message;
|
||||||
|
if (!message) {
|
||||||
|
console.log("[GowlersTracking] buildSourceLabel: No message in options");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[GowlersTracking] buildSourceLabel: Processing damage value:", value);
|
||||||
|
console.log("[GowlersTracking] buildSourceLabel: Message flags:", message.flags);
|
||||||
|
|
||||||
|
// Check for identifiedInfo (action name, item name)
|
||||||
|
if (message.flags?.pf1?.identifiedInfo) {
|
||||||
|
const identified = message.flags.pf1.identifiedInfo;
|
||||||
|
const actionName = identified.action || identified.actionName || identified.itemName;
|
||||||
|
const actorName = identified.actorName || "Unknown";
|
||||||
|
console.log("[GowlersTracking] Found identifiedInfo:", identified);
|
||||||
|
if (actionName) {
|
||||||
|
const label = value < 0 ? `Damage (${actorName}, ${actionName})` : `Healing (${actionName})`;
|
||||||
|
console.log("[GowlersTracking] Using identifiedInfo label:", label);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for chat card metadata (actor + item responsible)
|
||||||
|
if (message.flags?.pf1?.metadata) {
|
||||||
|
const meta = message.flags.pf1.metadata;
|
||||||
|
const actorName = meta.actor?.name || meta.actorName || "Unknown";
|
||||||
|
const itemName = meta.item?.name || meta.itemName || "Attack";
|
||||||
|
console.log("[GowlersTracking] Found metadata:", meta);
|
||||||
|
if (value < 0) {
|
||||||
|
const label = `Damage (${actorName}, ${itemName})`;
|
||||||
|
console.log("[GowlersTracking] Using metadata label:", label);
|
||||||
|
return label;
|
||||||
|
} else {
|
||||||
|
const label = `Healing (${itemName})`;
|
||||||
|
console.log("[GowlersTracking] Using metadata healing label:", label);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for flavor text (custom macros may only have flavor)
|
||||||
|
if (message.flavor) {
|
||||||
|
// Try to parse flavor for action names
|
||||||
|
const flavorMatch = message.flavor.match(/\*\*(.*?)\*\*|<strong>(.*?)<\/strong>/);
|
||||||
|
if (flavorMatch) {
|
||||||
|
const actionName = flavorMatch[1] || flavorMatch[2];
|
||||||
|
const label = value < 0 ? `Damage (${actionName})` : `Healing (${actionName})`;
|
||||||
|
console.log("[GowlersTracking] Using flavor text label:", label);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks
|
||||||
|
const fallback = value < 0 ? "Damage" : "Healing";
|
||||||
|
console.log("[GowlersTracking] Using fallback label:", fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Record damage source for later consumption
|
||||||
|
function noteDamageSource(value, options = {}) {
|
||||||
|
const actor = this; // 'this' is the actor whose HP is changing
|
||||||
|
console.log("[GowlersTracking] noteDamageSource called for", actor?.name, "with value:", value);
|
||||||
|
|
||||||
|
if (!actor?.id) {
|
||||||
|
console.log("[GowlersTracking] noteDamageSource: No actor ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = buildSourceLabel(value, options);
|
||||||
|
if (!label) {
|
||||||
|
console.log("[GowlersTracking] noteDamageSource: No label generated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ledgerState.sources = ledgerState.sources || new Map();
|
||||||
|
ledgerState.sources.set(actor.id, { label, ts: Date.now() });
|
||||||
|
console.log("[GowlersTracking] Damage source noted for", actor.name, "->", label);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[GowlersTracking] Combat hooks registered: createCombat, deleteCombat, updateCombat");
|
console.log("[GowlersTracking] Combat hooks registered: createCombat, deleteCombat, updateCombat");
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
@@ -338,11 +556,12 @@ function openHistoryDialog(actor, initialTab = "hp") {
|
|||||||
{
|
{
|
||||||
title: `${actor.name}: Tracking Log`,
|
title: `${actor.name}: Tracking Log`,
|
||||||
content,
|
content,
|
||||||
buttons: { close: { label: "Close" } },
|
buttons: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
width: 800,
|
width: 800,
|
||||||
height: "auto",
|
height: 550,
|
||||||
|
resizable: true,
|
||||||
classes: ["pf1-history-dialog"],
|
classes: ["pf1-history-dialog"],
|
||||||
render: (html) => {
|
render: (html) => {
|
||||||
// Handle both jQuery objects and DOM elements
|
// Handle both jQuery objects and DOM elements
|
||||||
@@ -411,13 +630,113 @@ function openHistoryDialog(actor, initialTab = "hp") {
|
|||||||
console.log("[GowlersTracking] Header not found or button already exists");
|
console.log("[GowlersTracking] Header not found or button already exists");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add resize button to header
|
||||||
|
const $header = $html.closest('.app, .window-app, .dialog, [role="dialog"]').find('.window-header');
|
||||||
|
if ($header.length && !$header.find('[data-history-resize-header]').length) {
|
||||||
|
const $resizeBtn = $('<button/>', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'history-resize-header-btn',
|
||||||
|
'data-history-resize-header': 'true',
|
||||||
|
title: 'Resize Dialog',
|
||||||
|
html: '<i class="fas fa-expand"></i>',
|
||||||
|
css: {
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#999',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px 8px',
|
||||||
|
marginRight: '4px',
|
||||||
|
fontSize: '16px',
|
||||||
|
transition: 'color 0.2s'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$resizeBtn.on('mouseenter', function() {
|
||||||
|
$(this).css('color', '#333');
|
||||||
|
}).on('mouseleave', function() {
|
||||||
|
$(this).css('color', '#999');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the close button and insert before it
|
||||||
|
const $closeBtn = $header.find('.close');
|
||||||
|
if ($closeBtn.length) {
|
||||||
|
$closeBtn.before($resizeBtn);
|
||||||
|
console.log("[GowlersTracking] Resize button added to header");
|
||||||
|
} else {
|
||||||
|
$header.append($resizeBtn);
|
||||||
|
console.log("[GowlersTracking] Resize button appended to header");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register dialog for live updates
|
||||||
|
ledgerState.openDialogs.set(actor.id, dialog);
|
||||||
|
console.log("[GowlersTracking] Dialog registered for actor:", actor.name);
|
||||||
|
|
||||||
|
// Remove from registry when dialog closes
|
||||||
|
const originalClose = dialog.close.bind(dialog);
|
||||||
|
dialog.close = function() {
|
||||||
|
ledgerState.openDialogs.delete(actor.id);
|
||||||
|
console.log("[GowlersTracking] Dialog unregistered for actor:", actor.name);
|
||||||
|
return originalClose();
|
||||||
|
};
|
||||||
|
|
||||||
dialog.render(true);
|
dialog.render(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to build XP breakdown tooltip
|
||||||
|
function buildXpBreakdownTooltip(actor, xpEntry) {
|
||||||
|
if (!xpEntry || !xpEntry.encounterId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encounters = actor.getFlag(FLAG_SCOPE, ENCOUNTER_FLAG) ?? [];
|
||||||
|
const encounter = encounters.find(e => e.encounterID === xpEntry.encounterId);
|
||||||
|
|
||||||
|
if (!encounter) {
|
||||||
|
return `${xpEntry.diff} XP from encounter`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build breakdown from encounter participants
|
||||||
|
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 participantsStr = participantNames.join(", ");
|
||||||
|
const more = encounter.participants.length > 2 ? ` +${encounter.participants.length - 2} more` : "";
|
||||||
|
breakdown.push(`${xpEntry.diff} from: ${participantsStr}${more}`);
|
||||||
|
} else {
|
||||||
|
breakdown.push(`${xpEntry.diff} XP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add encounter status
|
||||||
|
if (encounter.status) {
|
||||||
|
breakdown.push(`Status: ${encounter.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return breakdown.join("\n");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[GowlersTracking] Error building XP tooltip:", err);
|
||||||
|
return `${xpEntry.diff} XP`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildHistoryContent(actor, tabArg) {
|
function buildHistoryContent(actor, tabArg) {
|
||||||
const initialTab = tabArg ?? "hp"; // Explicitly capture the tab parameter
|
const initialTab = tabArg ?? "hp"; // Explicitly capture the tab parameter
|
||||||
console.log("[GowlersTracking] buildHistoryContent called with initialTab:", initialTab);
|
console.log("[GowlersTracking] buildHistoryContent called with initialTab:", initialTab);
|
||||||
@@ -449,7 +768,7 @@ function buildHistoryContent(actor, tabArg) {
|
|||||||
{
|
{
|
||||||
label: "Δ",
|
label: "Δ",
|
||||||
render: (entry) => entry.diff,
|
render: (entry) => entry.diff,
|
||||||
getTitle: (entry) => entry.damageBreakdown ? `${entry.damageBreakdown}` : ""
|
getTitle: (entry) => buildXpBreakdownTooltip(actor, entry)
|
||||||
},
|
},
|
||||||
{ label: "Source", render: (entry) => entry.source ?? "Manual" },
|
{ label: "Source", render: (entry) => entry.source ?? "Manual" },
|
||||||
{ label: "Encounter", render: (entry) => entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A" },
|
{ label: "Encounter", render: (entry) => entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A" },
|
||||||
@@ -496,8 +815,21 @@ 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" : "";
|
||||||
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" style="display:${display}">
|
const rowsPerPage = 10; // Default rows per page for history dialog
|
||||||
${renderHistoryTable(entries, cfg.columns, cfg.id)}
|
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)}
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
@@ -525,12 +857,18 @@ function buildHistoryContent(actor, tabArg) {
|
|||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHistoryTable(entries, columns, id) {
|
function renderHistoryTable(entries, columns, id, rowsPerPage = 10, currentPage = 1) {
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
return `<p class="history-empty">No ${id.toUpperCase()} history recorded.</p>`;
|
return `<p class="history-empty">No ${id.toUpperCase()} history recorded.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = entries
|
// Calculate pagination
|
||||||
|
const itemsToShow = rowsPerPage === "all" ? entries.length : rowsPerPage;
|
||||||
|
const startIdx = (currentPage - 1) * itemsToShow;
|
||||||
|
const endIdx = startIdx + itemsToShow;
|
||||||
|
const paginatedEntries = entries.slice(startIdx, endIdx);
|
||||||
|
|
||||||
|
const rows = paginatedEntries
|
||||||
.map(
|
.map(
|
||||||
(entry) => `
|
(entry) => `
|
||||||
<tr>
|
<tr>
|
||||||
@@ -561,6 +899,24 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
|
|
||||||
const diffValue = config.diff(previous, nextValue);
|
const diffValue = config.diff(previous, nextValue);
|
||||||
|
|
||||||
|
// COMPREHENSIVE DEBUG LOGGING FOR DAMAGE REPORTING
|
||||||
|
console.log("[GowlersTracking] ===== recordHistoryEntry DEBUG =====");
|
||||||
|
console.log("[GowlersTracking] statId:", statId);
|
||||||
|
console.log("[GowlersTracking] Previous:", previous, "Next:", nextValue, "Diff:", diffValue);
|
||||||
|
console.log("[GowlersTracking] Actor:", actor.name, "(" + actor.id + ")");
|
||||||
|
console.log("[GowlersTracking] userId:", userId);
|
||||||
|
console.log("[GowlersTracking] Full options object:", options);
|
||||||
|
console.log("[GowlersTracking] Full change object:", change);
|
||||||
|
|
||||||
|
// Log all keys in options for inspection
|
||||||
|
if (Object.keys(options).length > 0) {
|
||||||
|
console.log("[GowlersTracking] Options keys:", Object.keys(options));
|
||||||
|
for (const key of Object.keys(options)) {
|
||||||
|
console.log(`[GowlersTracking] options.${key}:`, options[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("[GowlersTracking] ===== end debug =====");
|
||||||
|
|
||||||
// Determine encounter ID: use active combat, or if none, check if combat just ended
|
// Determine encounter ID: use active combat, or if none, check if combat just ended
|
||||||
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"}`);
|
||||||
@@ -582,41 +938,38 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
let sourceDetails = "";
|
let sourceDetails = "";
|
||||||
let damageBreakdown = "";
|
let damageBreakdown = "";
|
||||||
|
|
||||||
if (options?.pf1DamageData) {
|
// For HP damage, search recent messages for matching damage application
|
||||||
// Try to get attacker actor and item information
|
if (statId === "hp" && diffValue < 0) {
|
||||||
const attackerName = options?.pf1?.attackerName || "Unknown";
|
const damageAmount = Math.abs(diffValue);
|
||||||
const itemName = options?.pf1?.itemName || "Attack";
|
const now = Date.now();
|
||||||
source = "Damage";
|
|
||||||
sourceDetails = `${attackerName}, ${itemName}`;
|
|
||||||
|
|
||||||
// Extract damage breakdown if available
|
// Look for matching message in recent queue (within 4 seconds)
|
||||||
if (options?.pf1DamageData?.rolls) {
|
let matchedMessage = null;
|
||||||
const damageRolls = options.pf1DamageData.rolls;
|
for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) {
|
||||||
const breakdown = [];
|
const msg = ledgerState.recentMessages[i];
|
||||||
let total = 0;
|
const timeSinceMessage = now - msg.timestamp;
|
||||||
for (const roll of damageRolls) {
|
|
||||||
if (roll.damageType && roll.value) {
|
// Match if: within 4 seconds AND damage value is close (within 10% tolerance)
|
||||||
breakdown.push(`${roll.value} ${roll.damageType}`);
|
const valueMatch = Math.abs(msg.value - damageAmount) <= Math.max(1, damageAmount * 0.1);
|
||||||
total += parseInt(roll.value) || 0;
|
|
||||||
}
|
if (timeSinceMessage < 4000 && valueMatch) {
|
||||||
}
|
matchedMessage = msg;
|
||||||
if (breakdown.length > 0) {
|
console.log("[GowlersTracking] Found matching message:", msg.source, "value:", msg.value, "vs", damageAmount);
|
||||||
damageBreakdown = breakdown.join(", ");
|
ledgerState.recentMessages.splice(i, 1); // Remove matched message
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (options?.healing) {
|
|
||||||
const healerName = options?.pf1?.healerName || "Unknown";
|
if (matchedMessage) {
|
||||||
const itemName = options?.pf1?.itemName || "Healing";
|
source = matchedMessage.source;
|
||||||
source = "Healing";
|
damageBreakdown = `${damageAmount} HP`;
|
||||||
sourceDetails = `${healerName}, ${itemName}`;
|
console.log(`[GowlersTracking] Using matched damage source:`, source);
|
||||||
damageBreakdown = `Healed for ${Math.abs(diffValue)} HP`;
|
}
|
||||||
} else if (options?.pf1?.actionType === "spell") {
|
}
|
||||||
source = "Spell";
|
|
||||||
sourceDetails = options?.pf1?.itemName || "Spell";
|
// Fallback for unmatched HP changes
|
||||||
} else if (statId === "xp" && diffValue > 0) {
|
if (source === "Manual" && statId === "hp") {
|
||||||
source = "XP Award";
|
// Check if it's damage or healing based on the diff (fallback for manual HP changes)
|
||||||
} else if (statId === "hp") {
|
|
||||||
// Check if it's damage or healing based on the diff
|
|
||||||
const hpDiff = parseInt(diffValue);
|
const hpDiff = parseInt(diffValue);
|
||||||
if (hpDiff < 0) {
|
if (hpDiff < 0) {
|
||||||
source = "Damage";
|
source = "Damage";
|
||||||
@@ -625,6 +978,14 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
source = "Healing";
|
source = "Healing";
|
||||||
damageBreakdown = `${hpDiff} healing`;
|
damageBreakdown = `${hpDiff} healing`;
|
||||||
}
|
}
|
||||||
|
} else if (statId === "xp" && diffValue > 0) {
|
||||||
|
// XP gains - could be from encounter end or manual award
|
||||||
|
source = "XP Award";
|
||||||
|
damageBreakdown = `${diffValue} XP`;
|
||||||
|
} else if (statId === "currency" && diffValue !== 0) {
|
||||||
|
// Currency changes
|
||||||
|
source = diffValue > 0 ? "Gained" : "Spent";
|
||||||
|
damageBreakdown = `${Math.abs(diffValue)} currency`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format source with details if available
|
// Format source with details if available
|
||||||
@@ -650,6 +1011,9 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
|
|
||||||
await actor.update({ [`flags.${FLAG_SCOPE}.${config.flag}`]: existing }, { [MODULE_ID]: true });
|
await actor.update({ [`flags.${FLAG_SCOPE}.${config.flag}`]: existing }, { [MODULE_ID]: true });
|
||||||
|
|
||||||
|
// Refresh any open dialogs for this actor
|
||||||
|
refreshOpenDialogs(actor.id);
|
||||||
|
|
||||||
if (shouldSendChat(actor.id, statId)) {
|
if (shouldSendChat(actor.id, statId)) {
|
||||||
sendChatNotification(statId, actor, previous, nextValue, entry);
|
sendChatNotification(statId, actor, previous, nextValue, entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"title": "Gowler's Tracking Ledger",
|
"title": "Gowler's Tracking Ledger",
|
||||||
"description": "Adds HP/XP/Currency log buttons to PF1 sheets and opens the tracking dialog preloaded with the actor's logs.",
|
"description": "Adds HP/XP/Currency log buttons to PF1 sheets and opens the tracking dialog preloaded with the actor's logs.",
|
||||||
"version": "0.1.13",
|
"version": "0.1.18",
|
||||||
"authors": [
|
"authors": [
|
||||||
{ "name": "Gowler", "url": "https://foundryvtt.com" }
|
{ "name": "Gowler", "url": "https://foundryvtt.com" }
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user