Bump version and finalize history clearing
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
|
||||
const MODULE_ID = "gowlers-tracking-ledger";
|
||||
const MODULE_VERSION = "1.1.1";
|
||||
const MODULE_VERSION = "1.2.1";
|
||||
const TRACK_SETTING = "actorSettings";
|
||||
const FLAG_SCOPE = "world";
|
||||
const MAX_HISTORY_ROWS = 100;
|
||||
@@ -9,8 +9,8 @@ const BUTTON_TITLE = "Open Log";
|
||||
const BUTTON_ICON = '<i class="fas fa-clock-rotate-left"></i>';
|
||||
const BUTTON_STYLE =
|
||||
"position:absolute;right:6px;top:4px;border:none;background:transparent;padding:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;";
|
||||
const DEFAULT_TRACKING = Object.freeze({ hp: true, xp: true, currency: true });
|
||||
const DEFAULT_CHAT = Object.freeze({ hp: false, xp: false, currency: false });
|
||||
const DEFAULT_TRACKING = Object.freeze({ hp: true, outgoing: true, xp: true, currency: true, encounters: true });
|
||||
const DEFAULT_CHAT = Object.freeze({ hp: false, outgoing: false, xp: false, currency: false, encounters: false, chatAll: false });
|
||||
const SETTINGS_VERSION = 2;
|
||||
const COIN_ORDER = ["pp", "gp", "sp", "cp"];
|
||||
const ENCOUNTER_FLAG = "pf1EncounterHistory";
|
||||
@@ -85,6 +85,7 @@ const ledgerState = {
|
||||
historyTabState: new Map(), // actorId -> active tab id
|
||||
damageOverlay: null,
|
||||
damageMeterIncludeNPCs: true,
|
||||
damageMeterMode: "encounter", // "encounter" or "all"
|
||||
};
|
||||
|
||||
function getHistoryPageState(actorId, tabId) {
|
||||
@@ -275,6 +276,10 @@ Hooks.once("ready", async () => {
|
||||
ledgerState.damageMeterIncludeNPCs = !!checked;
|
||||
refreshDamageMeterOverlay();
|
||||
};
|
||||
window.GowlersTrackingDamageMeterSetMode = (mode) => {
|
||||
ledgerState.damageMeterMode = mode === "all" ? "all" : "encounter";
|
||||
refreshDamageMeterOverlay();
|
||||
};
|
||||
});
|
||||
|
||||
async function initializeModule() {
|
||||
@@ -587,6 +592,20 @@ function registerSceneControls() {
|
||||
button: true,
|
||||
onClick: () => openDamageMeterOverlay(),
|
||||
});
|
||||
tokenControls.tools.push({
|
||||
name: "history-ledger",
|
||||
title: "History Ledger",
|
||||
icon: "fas fa-scroll",
|
||||
button: true,
|
||||
onClick: () => {
|
||||
const actor = canvas?.tokens?.controlled?.[0]?.actor ?? null;
|
||||
if (actor) {
|
||||
openHistoryDialog(actor, "hp");
|
||||
} else {
|
||||
ui.notifications?.warn?.("Select a token to open its history ledger.");
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -611,7 +630,7 @@ function openDamageMeterOverlay() {
|
||||
},
|
||||
}, {
|
||||
id: `${MODULE_ID}-damage-meter`,
|
||||
width: 300,
|
||||
width: 420,
|
||||
height: "auto",
|
||||
resizable: true,
|
||||
});
|
||||
@@ -969,7 +988,7 @@ function buildHistoryContent(actor, tabArg) {
|
||||
const configs = [
|
||||
{
|
||||
id: "hp",
|
||||
label: "HP",
|
||||
label: "Incoming",
|
||||
flag: STAT_CONFIGS.hp.flag,
|
||||
columns: [
|
||||
{ label: "Timestamp", render: (entry) => formatDate(entry.timestamp) },
|
||||
@@ -985,11 +1004,12 @@ function buildHistoryContent(actor, tabArg) {
|
||||
},
|
||||
{
|
||||
id: "damage",
|
||||
label: "Damage",
|
||||
label: "Outgoing",
|
||||
flag: DAMAGE_DEALT_FLAG,
|
||||
columns: [
|
||||
{ label: "Timestamp", render: (entry) => formatDate(entry.timestamp) },
|
||||
{ label: "Dmg", render: (entry) => entry.amount != null ? `${entry.amount}` : "" },
|
||||
{ label: "Dmg", render: (entry) => entry.type === "healing" ? "" : (entry.amount != null ? `${entry.amount}` : "") },
|
||||
{ label: "Heal", render: (entry) => entry.type === "healing" ? `${entry.amount}` : "" },
|
||||
{
|
||||
label: "Details",
|
||||
render: (entry) => {
|
||||
@@ -1291,6 +1311,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
||||
source: source,
|
||||
breakdown: damageBreakdown,
|
||||
encounterId: encounterId,
|
||||
type: "damage",
|
||||
damageDetails: matchedMessage.damageDetails ?? null,
|
||||
};
|
||||
recordDamageDealt(attacker, dealtEntry);
|
||||
@@ -1300,6 +1321,33 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
||||
}
|
||||
}
|
||||
}
|
||||
// For HP healing, also try to match recent messages (same mechanism as damage)
|
||||
else if (statId === "hp" && diffValue > 0) {
|
||||
const healAmount = Math.abs(diffValue);
|
||||
const now = Date.now();
|
||||
|
||||
for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) {
|
||||
const msg = ledgerState.recentMessages[i];
|
||||
const timeSinceMessage = now - msg.timestamp;
|
||||
const valueMatch = Math.abs(Math.abs(msg.value) - healAmount) <= Math.max(1, healAmount * 0.1);
|
||||
|
||||
if (timeSinceMessage < 4000 && valueMatch) {
|
||||
matchedMessage = msg;
|
||||
console.log("[GowlersTracking] Found matching healing message:", msg.source, "value:", msg.value, "vs", healAmount);
|
||||
ledgerState.recentMessages.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedMessage) {
|
||||
source = matchedMessage.source;
|
||||
damageBreakdown =
|
||||
formatDamageBreakdown(matchedMessage.damageDetails, healAmount) ||
|
||||
matchedMessage.damageDetails?.breakdown ||
|
||||
`${healAmount} healing`;
|
||||
console.log(`[GowlersTracking] Using matched healing source:`, source);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unmatched HP changes
|
||||
if (source === "Manual" && statId === "hp") {
|
||||
@@ -1312,6 +1360,31 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
||||
source = "Healing";
|
||||
damageBreakdown = `${hpDiff} healing`;
|
||||
}
|
||||
} else if (statId === "hp" && diffValue > 0) {
|
||||
// Record outgoing healing (simple)
|
||||
const metadata = change?.metadata ?? options?.message?.flags?.pf1?.metadata ?? {};
|
||||
const healer = resolveActorFromMetadataSafe(metadata) || resolveActorFromMetadataSafe(matchedMessage?.message?.flags?.pf1?.metadata) || actor; // fallback to self
|
||||
if (matchedMessage) {
|
||||
source = matchedMessage.source;
|
||||
damageBreakdown =
|
||||
formatDamageBreakdown(matchedMessage.damageDetails, diffValue) ||
|
||||
matchedMessage.damageDetails?.breakdown ||
|
||||
`${diffValue} healing`;
|
||||
} else {
|
||||
source = healer?.name ? `${healer.name} -> Healing` : "Healing";
|
||||
damageBreakdown = `${diffValue} healing`;
|
||||
}
|
||||
const healEntry = {
|
||||
timestamp: Date.now(),
|
||||
amount: diffValue,
|
||||
target: actor.name,
|
||||
source: source,
|
||||
breakdown: damageBreakdown,
|
||||
encounterId: encounterId,
|
||||
type: "healing",
|
||||
damageDetails: matchedMessage?.damageDetails ?? null,
|
||||
};
|
||||
recordDamageDealt(healer, healEntry);
|
||||
} else if (statId === "xp" && diffValue > 0) {
|
||||
// XP gains - encounter or manual award
|
||||
source = options?.source ?? (encounterId ? "Encounter XP Award" : "XP Award");
|
||||
@@ -1525,6 +1598,7 @@ function formatDamagePartsWithIcons(parts) {
|
||||
force: { icon: "ra ra-crystal-ball", color: "#845ef7" },
|
||||
negative: { icon: "ra ra-skull", color: "#7950f2" },
|
||||
positive: { icon: "ra ra-sun", color: "#fab005" },
|
||||
healing: { icon: "ra ra-health", color: "#4caf50" },
|
||||
precision: { icon: "ra ra-target-arrows", color: "#000" },
|
||||
nonlethal: { icon: "ra ra-hand", color: "#000" },
|
||||
untyped: { icon: "ra ra-uncertainty", color: "#666" },
|
||||
@@ -1553,6 +1627,7 @@ function renderDamageBar(composition = [], total = 0) {
|
||||
force: { icon: "ra ra-crystal-ball", color: "#845ef7" },
|
||||
negative: { icon: "ra ra-skull", color: "#7950f2" },
|
||||
positive: { icon: "ra ra-sun", color: "#fab005" },
|
||||
healing: { icon: "ra ra-health", color: "#4caf50" },
|
||||
precision: { icon: "ra ra-target-arrows", color: "#000" },
|
||||
nonlethal: { icon: "ra ra-hand", color: "#000" },
|
||||
untyped: { icon: "ra ra-uncertainty", color: "#666" },
|
||||
@@ -1684,7 +1759,8 @@ function refreshDamageMeterOverlay() {
|
||||
}
|
||||
|
||||
function computeDamageMeterData() {
|
||||
const currentEncounterId = game.combat?.id ?? ledgerState.lastCombatId ?? null;
|
||||
const mode = ledgerState.damageMeterMode === "all" ? "all" : "encounter";
|
||||
const currentEncounterId = mode === "encounter" ? (game.combat?.id ?? ledgerState.lastCombatId ?? null) : null;
|
||||
const actorsMap = new Map();
|
||||
|
||||
// Directory actors
|
||||
@@ -1724,10 +1800,13 @@ function computeDamageMeterData() {
|
||||
if (!filtered.length) continue;
|
||||
|
||||
const typeTotals = new Map();
|
||||
let total = 0;
|
||||
let totalDamage = 0;
|
||||
let totalHealing = 0;
|
||||
for (const e of filtered) {
|
||||
const amount = Number(e.amount) || 0;
|
||||
total += amount;
|
||||
if (e.type === "healing") totalHealing += amount;
|
||||
else totalDamage += amount;
|
||||
|
||||
const parts = e.damageDetails?.parts;
|
||||
if (Array.isArray(parts) && parts.length) {
|
||||
for (const p of parts) {
|
||||
@@ -1736,8 +1815,9 @@ function computeDamageMeterData() {
|
||||
typeTotals.set(t, prev + (Number(p.total) || 0));
|
||||
}
|
||||
} else {
|
||||
const prev = typeTotals.get("untyped") ?? 0;
|
||||
typeTotals.set("untyped", prev + amount);
|
||||
const key = e.type === "healing" ? "healing" : "untyped";
|
||||
const prev = typeTotals.get(key) ?? 0;
|
||||
typeTotals.set(key, prev + amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1749,7 +1829,9 @@ function computeDamageMeterData() {
|
||||
totals.push({
|
||||
actorId: actor.id,
|
||||
name: actor.name,
|
||||
total,
|
||||
total: totalDamage,
|
||||
healing: totalHealing,
|
||||
overall: totalDamage + totalHealing,
|
||||
hits: filtered.length,
|
||||
last: filtered[0],
|
||||
img,
|
||||
@@ -1757,29 +1839,44 @@ function computeDamageMeterData() {
|
||||
});
|
||||
}
|
||||
|
||||
totals.sort((a, b) => b.total - a.total);
|
||||
totals.sort((a, b) => (b.overall ?? b.total) - (a.overall ?? a.total));
|
||||
const grandTotal = totals.reduce((s, t) => s + t.total, 0);
|
||||
return { totals, grandTotal, encounterId: currentEncounterId };
|
||||
const grandHealing = totals.reduce((s, t) => s + (t.healing ?? 0), 0);
|
||||
return { totals, grandTotal, grandHealing, encounterId: currentEncounterId };
|
||||
}
|
||||
|
||||
function buildDamageMeterContent() {
|
||||
const { totals, grandTotal, encounterId } = computeDamageMeterData();
|
||||
const { totals, grandTotal, grandHealing, encounterId } = computeDamageMeterData();
|
||||
const encounterLabel = encounterId ? `Encounter: ${encounterId.slice(0, 8)}` : "All history";
|
||||
const includeNPCs = ledgerState.damageMeterIncludeNPCs;
|
||||
|
||||
const npcToggle = `<label style="font-size:12px; display:inline-flex; align-items:center; gap:4px; margin:0;">
|
||||
<input type="checkbox" ${includeNPCs ? "checked" : ""} onchange="window.GowlersTrackingDamageMeterToggleNPCs?.(this.checked)">
|
||||
Include NPCs
|
||||
</label>`;
|
||||
const modeToggle = `
|
||||
<div style="display:inline-flex; border:1px solid #b5b3a4; border-radius:12px; overflow:hidden; height:24px;">
|
||||
<button type="button" onclick="window.GowlersTrackingDamageMeterSetMode('encounter')" style="border:none; background:${ledgerState.damageMeterMode === "encounter" ? "#d6d3c8" : "#f5f4ef"}; padding:2px 8px; cursor:pointer; font-size:11px;">Encounter</button>
|
||||
<button type="button" onclick="window.GowlersTrackingDamageMeterSetMode('all')" style="border:none; border-left:1px solid #b5b3a4; background:${ledgerState.damageMeterMode === "all" ? "#d6d3c8" : "#f5f4ef"}; padding:2px 8px; cursor:pointer; font-size:11px;">All-Time</button>
|
||||
</div>`;
|
||||
|
||||
const rows = totals.length
|
||||
? totals
|
||||
.map(
|
||||
(t, idx) => {
|
||||
const max = totals[0]?.total || 1;
|
||||
const pct = Math.max(1, Math.round((t.total / max) * 100));
|
||||
const bar = renderDamageBar(t.composition, t.total) || `<div style="background:#e0e0e0; border-radius:4px; overflow:hidden; height:10px; margin-bottom:4px;"><div style="width:${pct}%; background:#ff9800; height:10px;"></div></div>`;
|
||||
const max = totals[0]?.overall || totals[0]?.total || 1;
|
||||
const pct = Math.max(1, Math.round(((t.overall ?? t.total) / max) * 100));
|
||||
const innerBar =
|
||||
renderDamageBar(t.composition, t.overall ?? t.total) ||
|
||||
`<div style="background:#e0e0e0; border-radius:4px; overflow:hidden; height:10px;"><div style="width:${pct}%; background:#ff9800; height:10px;"></div></div>`;
|
||||
const bar = `<div style="width:${pct}%; min-width:4px; max-width:100%;">${innerBar}</div>`;
|
||||
const avatar = t.img ? `<img src="${t.img}" style="width:18px; height:18px; object-fit:cover; border-radius:3px; margin-right:6px;" />` : "";
|
||||
return `
|
||||
<tr>
|
||||
<td style="padding:4px 6px;">${idx + 1}</td>
|
||||
<td style="padding:4px 6px; display:flex; align-items:center;">${avatar}${t.name}</td>
|
||||
<td style="padding:4px 6px; text-align:right;">${t.total}</td>
|
||||
<td style="padding:4px 6px; text-align:right;">${t.healing}</td>
|
||||
<td style="padding:4px 6px; text-align:right;">${t.hits}</td>
|
||||
<td style="padding:4px 6px; max-width: 220px;">
|
||||
${bar}
|
||||
@@ -1789,26 +1886,23 @@ function buildDamageMeterContent() {
|
||||
}
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="5" style="padding:6px; text-align:center; opacity:0.7;">No damage recorded${encounterId ? " for this encounter" : ""}.</td></tr>`;
|
||||
|
||||
const npcToggle = `<label style="font-size:12px; display:inline-flex; align-items:center; gap:4px; margin-left:6px;">
|
||||
<input type="checkbox" ${includeNPCs ? "checked" : ""} onchange="window.GowlersTrackingDamageMeterToggleNPCs?.(this.checked)">
|
||||
Include NPCs
|
||||
</label>`;
|
||||
: `<tr><td colspan="6" style="padding:6px; text-align:center; opacity:0.7;">No damage recorded${encounterId ? " for this encounter" : ""}.</td></tr>`;
|
||||
|
||||
return `
|
||||
<div style="padding:8px; font-size: 13px; line-height: 1.4;">
|
||||
<div style="font-weight:bold; margin-bottom:4px;">Damage Meter</div>
|
||||
<div style="margin-bottom:6px; display:flex; align-items:center; gap:12px;">
|
||||
<span>${encounterLabel}</span>
|
||||
${npcToggle}
|
||||
<div style="margin-bottom:6px; display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<div style="display:flex; align-items:center; gap:6px;">${modeToggle}</div>
|
||||
<div style="display:flex; align-items:center; gap:6px;">${npcToggle}</div>
|
||||
<div style="display:flex; align-items:center; gap:6px;">${encounterLabel}</div>
|
||||
</div>
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left; padding:4px 6px;">#</th>
|
||||
<th style="text-align:left; padding:4px 6px;">Actor</th>
|
||||
<th style="text-align:right; padding:4px 6px;">Total</th>
|
||||
<th style="text-align:right; padding:4px 6px;">Damage</th>
|
||||
<th style="text-align:right; padding:4px 6px;">Healing</th>
|
||||
<th style="text-align:right; padding:4px 6px;">Hits</th>
|
||||
<th style="text-align:left; padding:4px 6px;">Last Breakdown</th>
|
||||
</tr>
|
||||
@@ -1821,6 +1915,7 @@ function buildDamageMeterContent() {
|
||||
<td></td>
|
||||
<td style="padding:4px 6px; font-weight:bold;">Total</td>
|
||||
<td style="padding:4px 6px; text-align:right; font-weight:bold;">${grandTotal}</td>
|
||||
<td style="padding:4px 6px; text-align:right; font-weight:bold;">${grandHealing}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -1853,6 +1948,7 @@ function createActorConfig(source = null) {
|
||||
if (source.version === SETTINGS_VERSION) {
|
||||
config.tracking = { ...config.tracking, ...(source.tracking ?? {}) };
|
||||
config.chat = { ...config.chat, ...(source.chat ?? {}) };
|
||||
config.chat.chatAll = source.chat?.chatAll ?? Object.values(source.chat ?? {}).some(Boolean);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1912,7 +2008,8 @@ function getActorTracking(actorId) {
|
||||
|
||||
function getActorChat(actorId) {
|
||||
const entry = getActorConfig(actorId);
|
||||
return { ...entry.chat };
|
||||
const hasAny = Object.values(entry.chat ?? {}).some(Boolean);
|
||||
return { ...entry.chat, chatAll: entry.chat?.chatAll ?? hasAny };
|
||||
}
|
||||
|
||||
async function ensureActorConfig(actor) {
|
||||
@@ -1931,8 +2028,9 @@ async function setActorTracking(actorId, partial) {
|
||||
const entry = getActorConfig(actorId);
|
||||
entry.tracking = {
|
||||
hp: partial.hp ?? entry.tracking.hp,
|
||||
xp: partial.xp ?? entry.tracking.xp,
|
||||
outgoing: partial.outgoing ?? entry.tracking.outgoing,
|
||||
currency: partial.currency ?? entry.tracking.currency,
|
||||
encounters: partial.encounters ?? entry.tracking.encounters,
|
||||
};
|
||||
await saveActorSettings(settings);
|
||||
const actor = game.actors.get(actorId);
|
||||
@@ -1943,17 +2041,32 @@ async function setActorChat(actorId, partial) {
|
||||
if (!actorId) return;
|
||||
const settings = getSettingsCache();
|
||||
const entry = getActorConfig(actorId);
|
||||
const chatAll = partial.chatAll;
|
||||
const mergedChat = { ...entry.chat, ...partial };
|
||||
if (chatAll !== undefined && chatAll !== null) {
|
||||
mergedChat.hp = !!chatAll;
|
||||
mergedChat.outgoing = !!chatAll;
|
||||
mergedChat.xp = !!chatAll;
|
||||
mergedChat.currency = !!chatAll;
|
||||
mergedChat.encounters = !!chatAll;
|
||||
mergedChat.chatAll = !!chatAll;
|
||||
} else {
|
||||
mergedChat.chatAll = !!mergedChat.chatAll;
|
||||
}
|
||||
entry.chat = {
|
||||
hp: partial.hp ?? entry.chat.hp,
|
||||
xp: partial.xp ?? entry.chat.xp,
|
||||
currency: partial.currency ?? entry.chat.currency,
|
||||
hp: mergedChat.hp,
|
||||
outgoing: mergedChat.outgoing,
|
||||
xp: mergedChat.xp,
|
||||
currency: mergedChat.currency,
|
||||
encounters: mergedChat.encounters,
|
||||
chatAll: mergedChat.chatAll,
|
||||
};
|
||||
await saveActorSettings(settings);
|
||||
}
|
||||
|
||||
function shouldSendChat(actorId, statId) {
|
||||
const chat = getActorChat(actorId);
|
||||
return !!chat[statId];
|
||||
return chat.chatAll ?? !!chat[statId];
|
||||
}
|
||||
|
||||
class TrackingLedgerConfig extends FormApplication {
|
||||
@@ -2009,11 +2122,13 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
const start = this._page * pageSize;
|
||||
const pageItems = filtered.slice(start, start + pageSize).map((ref) => {
|
||||
const entry = settings[ref.id] ?? createActorConfig();
|
||||
const chatAll = entry.chat?.chatAll ?? false;
|
||||
return {
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
tracking: { ...entry.tracking },
|
||||
chat: { ...entry.chat },
|
||||
chat: { ...entry.chat, chatAll },
|
||||
chatAll: chatAll,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2033,6 +2148,12 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
}));
|
||||
|
||||
const totalPagesDisplay = totalActors ? totalPages : 0;
|
||||
const allEntries = Object.values(settings);
|
||||
const allIncoming = allEntries.length ? allEntries.every((e) => e.tracking?.hp) : false;
|
||||
const allOutgoing = allEntries.length ? allEntries.every((e) => e.tracking?.outgoing) : false;
|
||||
const allCurrency = allEntries.length ? allEntries.every((e) => e.tracking?.currency) : false;
|
||||
const allEncounters = allEntries.length ? allEntries.every((e) => e.tracking?.encounters) : false;
|
||||
const allChatAll = allEntries.length ? allEntries.every((e) => e.chat?.chatAll ?? false) : false;
|
||||
|
||||
return {
|
||||
actors: pageItems,
|
||||
@@ -2048,6 +2169,12 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
hasPrev,
|
||||
hasNext,
|
||||
displayPage: totalActors ? this._page + 1 : 0,
|
||||
moduleVersion: MODULE_VERSION,
|
||||
allIncoming,
|
||||
allOutgoing,
|
||||
allCurrency,
|
||||
allEncounters,
|
||||
allChatAll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2065,25 +2192,40 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
const entry = getActorConfig(actorId);
|
||||
const nextTracking = {
|
||||
hp: checkboxValue(cfg.tracking?.hp, false),
|
||||
xp: checkboxValue(cfg.tracking?.xp, false),
|
||||
outgoing: checkboxValue(cfg.tracking?.outgoing, false),
|
||||
xp: entry.tracking?.xp ?? true,
|
||||
currency: checkboxValue(cfg.tracking?.currency, false),
|
||||
encounters: checkboxValue(cfg.tracking?.encounters, false),
|
||||
};
|
||||
const chatAll = checkboxValue(cfg.chatAll, false);
|
||||
const nextChat = {
|
||||
hp: checkboxValue(cfg.chat?.hp, false),
|
||||
xp: checkboxValue(cfg.chat?.xp, false),
|
||||
currency: checkboxValue(cfg.chat?.currency, false),
|
||||
hp: chatAll,
|
||||
outgoing: chatAll,
|
||||
xp: chatAll,
|
||||
currency: chatAll,
|
||||
encounters: chatAll,
|
||||
chatAll: chatAll,
|
||||
};
|
||||
|
||||
if (
|
||||
entry.tracking.hp !== nextTracking.hp ||
|
||||
entry.tracking.outgoing !== nextTracking.outgoing ||
|
||||
entry.tracking.xp !== nextTracking.xp ||
|
||||
entry.tracking.currency !== nextTracking.currency
|
||||
entry.tracking.currency !== nextTracking.currency ||
|
||||
entry.tracking.encounters !== nextTracking.encounters
|
||||
) {
|
||||
entry.tracking = nextTracking;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (entry.chat.hp !== nextChat.hp || entry.chat.xp !== nextChat.xp || entry.chat.currency !== nextChat.currency) {
|
||||
if (
|
||||
entry.chat.hp !== nextChat.hp ||
|
||||
entry.chat.outgoing !== nextChat.outgoing ||
|
||||
entry.chat.xp !== nextChat.xp ||
|
||||
entry.chat.currency !== nextChat.currency ||
|
||||
entry.chat.encounters !== nextChat.encounters ||
|
||||
entry.chat.chatAll !== nextChat.chatAll
|
||||
) {
|
||||
entry.chat = nextChat;
|
||||
dirty = true;
|
||||
}
|
||||
@@ -2142,6 +2284,109 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
TrackingLedgerConfig._lastPage = this._page;
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
html.find("[data-action=\"toggle-all\"]").on("change", async (event) => {
|
||||
const target = event.currentTarget.dataset.target;
|
||||
const value = event.currentTarget.checked;
|
||||
if (!target) return;
|
||||
const settings = getSettingsCache();
|
||||
let dirty = false;
|
||||
for (const [actorId, entry] of Object.entries(settings)) {
|
||||
if (!entry.tracking || !entry.chat) continue;
|
||||
switch (target) {
|
||||
case "tracking.hp":
|
||||
if (entry.tracking.hp !== value) {
|
||||
entry.tracking.hp = value;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
case "tracking.outgoing":
|
||||
if (entry.tracking.outgoing !== value) {
|
||||
entry.tracking.outgoing = value;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
case "tracking.currency":
|
||||
if (entry.tracking.currency !== value) {
|
||||
entry.tracking.currency = value;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
case "tracking.encounters":
|
||||
if (entry.tracking.encounters !== value) {
|
||||
entry.tracking.encounters = value;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
case "chat.chatAll":
|
||||
if (
|
||||
entry.chat.chatAll !== value ||
|
||||
entry.chat.hp !== value ||
|
||||
entry.chat.outgoing !== value ||
|
||||
entry.chat.currency !== value ||
|
||||
entry.chat.encounters !== value
|
||||
) {
|
||||
entry.chat.chatAll = value;
|
||||
entry.chat.hp = value;
|
||||
entry.chat.outgoing = value;
|
||||
entry.chat.currency = value;
|
||||
entry.chat.encounters = value;
|
||||
entry.chat.xp = value;
|
||||
dirty = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dirty) {
|
||||
await saveActorSettings(settings);
|
||||
this.render(false);
|
||||
}
|
||||
});
|
||||
|
||||
html.find("[data-action=\"clear-all-history\"]").on("click", async (event) => {
|
||||
event.preventDefault();
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: "Clear All Histories",
|
||||
content: "<p>Remove all stored ledger history for all actors?</p>",
|
||||
yes: () => true,
|
||||
no: () => false,
|
||||
defaultYes: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const actors = collectAllActorDocuments();
|
||||
const tokens = collectAllTokens();
|
||||
for (const actor of actors) await clearDocumentHistory(actor);
|
||||
for (const token of tokens) await clearDocumentHistory(token);
|
||||
ui.notifications?.info?.("History cleared for all actors.");
|
||||
} catch (err) {
|
||||
console.error("[GowlersTracking] Failed to clear all histories", err);
|
||||
ui.notifications?.error?.("Failed to clear all histories; see console.");
|
||||
}
|
||||
});
|
||||
|
||||
html.find("[data-action=\"clear-history\"]").on("click", async (event) => {
|
||||
event.preventDefault();
|
||||
const actorId = event.currentTarget.dataset.actorId;
|
||||
if (!actorId) return;
|
||||
const actor = game.actors.get(actorId);
|
||||
if (!actor) return;
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: "Clear History",
|
||||
content: `<p>Remove all stored ledger history (incoming, outgoing, XP, currency, encounters) for <strong>${actor.name}</strong>?</p>`,
|
||||
yes: () => true,
|
||||
no: () => false,
|
||||
defaultYes: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await clearDocumentHistory(actor);
|
||||
ui.notifications?.info?.(`History cleared for ${actor.name}.`);
|
||||
} catch (err) {
|
||||
console.error("[GowlersTracking] Failed to clear history", err);
|
||||
ui.notifications?.error?.("Failed to clear history for actor; see console.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static openForActor(actorId) {
|
||||
@@ -2156,6 +2401,44 @@ class TrackingLedgerConfig extends FormApplication {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDocumentHistory(doc) {
|
||||
if (!doc?.unsetFlag) return;
|
||||
const flagKeys = [
|
||||
...Object.values(STAT_CONFIGS).map((cfg) => cfg.flag),
|
||||
DAMAGE_DEALT_FLAG,
|
||||
ENCOUNTER_FLAG,
|
||||
];
|
||||
for (const key of flagKeys) {
|
||||
try {
|
||||
await doc.unsetFlag(FLAG_SCOPE, key);
|
||||
} catch (err) {
|
||||
console.warn(`[GowlersTracking] Failed to clear flag ${key} for ${doc.name ?? doc.id}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectAllActorDocuments() {
|
||||
const actors = new Map();
|
||||
for (const a of game.actors.contents ?? []) {
|
||||
actors.set(a.id, a);
|
||||
}
|
||||
for (const scene of game.scenes ?? []) {
|
||||
for (const token of scene.tokens ?? []) {
|
||||
const a = token.actor;
|
||||
if (a?.id && !actors.has(a.id)) actors.set(a.id, a);
|
||||
}
|
||||
}
|
||||
return Array.from(actors.values());
|
||||
}
|
||||
|
||||
function collectAllTokens() {
|
||||
const tokens = [];
|
||||
for (const scene of game.scenes ?? []) {
|
||||
for (const token of scene.tokens ?? []) tokens.push(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update encounter summary when combat starts
|
||||
*/
|
||||
@@ -2291,10 +2574,22 @@ function sendChatNotification(statId, actor, previous, nextValue, entry) {
|
||||
const title = titles[statId] ?? statId.toUpperCase();
|
||||
const prevText = config.formatValue(previous);
|
||||
const nextText = entry.value;
|
||||
const encounter = entry.encounterId ? entry.encounterId.slice(0, 8) : "N/A";
|
||||
const source = entry.source ?? "Manual";
|
||||
const partsHtml = entry.damageDetails?.parts?.length ? formatDamagePartsWithIcons(entry.damageDetails.parts) : "";
|
||||
const details = entry.damageBreakdown || entry.breakdown || "";
|
||||
const detailHtml = partsHtml || details ? `<div><strong>Details:</strong> ${partsHtml || details}</div>` : "";
|
||||
|
||||
const content = `
|
||||
<strong>${title} Log</strong><br>
|
||||
${actor.name}: ${prevText} -> ${nextText} (${entry.diff})<br>
|
||||
<em>User:</em> ${entry.user ?? "System"}
|
||||
<div style="font-family: var(--font-primary); font-size: 12px; line-height: 1.4;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${title} Update</div>
|
||||
<div><strong>Actor:</strong> ${actor.name}</div>
|
||||
<div><strong>Value:</strong> ${prevText} → ${nextText} (${entry.diff})</div>
|
||||
<div><strong>Source:</strong> ${source}</div>
|
||||
<div><strong>Encounter:</strong> ${encounter}</div>
|
||||
${detailHtml}
|
||||
<div style="opacity: 0.8;"><strong>User:</strong> ${entry.user ?? "System"}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
ChatMessage.create({
|
||||
|
||||
@@ -34,21 +34,29 @@
|
||||
<table class="tracking-ledger-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Actor</th>
|
||||
<th colspan="2">HP</th>
|
||||
<th colspan="2">XP</th>
|
||||
<th colspan="2">Currency</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Track</th>
|
||||
<th>Chat</th>
|
||||
<th>Track</th>
|
||||
<th>Chat</th>
|
||||
<th>Track</th>
|
||||
<th>Chat</th>
|
||||
<th>Actor</th>
|
||||
<th>Incoming (Track)</th>
|
||||
<th>Outgoing (Track)</th>
|
||||
<th>Currency (Track)</th>
|
||||
<th>Encounters (Track)</th>
|
||||
<th>Chat (All)</th>
|
||||
<th>History</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bulk-row" style="background:#f0f0f0;">
|
||||
<td><strong>All actors</strong></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-action="toggle-all" data-target="tracking.hp" {{#if allIncoming}}checked{{/if}}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-action="toggle-all" data-target="tracking.outgoing" {{#if allOutgoing}}checked{{/if}}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-action="toggle-all" data-target="tracking.currency" {{#if allCurrency}}checked{{/if}}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-action="toggle-all" data-target="tracking.encounters" {{#if allEncounters}}checked{{/if}}></td>
|
||||
<td style="text-align:center;"><input type="checkbox" data-action="toggle-all" data-target="chat.chatAll" {{#if allChatAll}}checked{{/if}}></td>
|
||||
<td style="text-align:center;">
|
||||
<button type="button" class="clear-history-btn" data-action="clear-all-history" style="padding:4px 8px; display:inline-flex; align-items:center; gap:6px;">
|
||||
<i class="fas fa-trash"></i><span>Clear All</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{#if actors.length}}
|
||||
{{#each actors}}
|
||||
<tr>
|
||||
@@ -60,30 +68,33 @@
|
||||
<input type="checkbox" name="actors.{{id}}.tracking.hp" {{#if tracking.hp}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.chat.hp" {{#if chat.hp}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.tracking.xp" {{#if tracking.xp}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.chat.xp" {{#if chat.xp}}checked{{/if}}>
|
||||
<input type="checkbox" name="actors.{{id}}.tracking.outgoing" {{#if tracking.outgoing}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.tracking.currency" {{#if tracking.currency}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.chat.currency" {{#if chat.currency}}checked{{/if}}>
|
||||
<input type="checkbox" name="actors.{{id}}.tracking.encounters" {{#if tracking.encounters}}checked{{/if}}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="actors.{{id}}.chatAll" {{#if chatAll}}checked{{/if}}>
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<button type="button" class="clear-history-btn" data-action="clear-history" data-actor-id="{{id}}" style="padding:4px 8px; display:inline-flex; align-items:center; gap:6px;">
|
||||
<i class="fas fa-trash"></i><span>Clear</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="7" class="empty">No actors match the current filter.</td>
|
||||
<td colspan="11" class="empty">No actors match the current filter.</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
<footer>
|
||||
<button type="submit"><i class="fas fa-save"></i> Save</button>
|
||||
<div style="flex:1; text-align:center; font-size:11px; opacity:0.7; margin-top:6px;">v{{moduleVersion}}</div>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user