diff --git a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js index b84bb121..1270046f 100644 --- a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js +++ b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js @@ -1,6 +1,6 @@ const MODULE_ID = "gowlers-tracking-ledger"; -const MODULE_VERSION = "0.1.25"; +const MODULE_VERSION = "0.1.27"; const TRACK_SETTING = "actorSettings"; const FLAG_SCOPE = "world"; const MAX_HISTORY_ROWS = 100; @@ -862,17 +862,48 @@ function buildXpBreakdownTooltip(actor, xpEntry) { return `${xpEntry.diff} XP from encounter`; } - // Build breakdown from encounter participants + // Build breakdown from encounter participants with per-NPC XP pulled from actor docs let breakdown = []; - if (encounter.participants && encounter.participants.length > 0) { - // Resolve participant UUIDs/IDs to actor names - const participantNames = encounter.participants.slice(0, 2).map(resolveParticipantName); - 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`); + const participants = encounter.participantDetails && encounter.participantDetails.length + ? encounter.participantDetails + : (encounter.participants ?? []).map((id) => ({ id, name: resolveParticipantName(id), isPlayer: false })); + + const resolved = participants.map((p) => { + const actorDoc = p.id ? game.actors.get(p.id) : null; + const name = p.name ?? actorDoc?.name ?? resolveParticipantName(p.id); + const isPlayer = p.isPlayer || actorDoc?.type === "character"; + const npcXp = !isPlayer ? Number(actorDoc?.system?.details?.xp?.value ?? 0) : 0; + return { ...p, name, isPlayer, npcXp }; + }); + + const players = resolved.filter((p) => p.isPlayer); + const monsters = resolved.filter((p) => !p.isPlayer); + + const playerNames = (players.length ? players : resolved).map((p) => p.name).filter(Boolean); + const monsterLines = monsters.map((m) => `${m.name ?? "Unknown"}${m.npcXp ? ` (${m.npcXp.toLocaleString()} XP)` : ""}`); + + const playerCount = playerNames.length || 1; + const totalEncounterXp = Math.max(0, xpEntry.diff * playerCount); + + const monsterXpTotal = monsters.reduce((sum, m) => sum + (Number.isFinite(m.npcXp) ? m.npcXp : 0), 0); + const bonusXp = Math.max(0, totalEncounterXp - monsterXpTotal); + + breakdown.push(`${xpEntry.diff} XP (per actor)`); + if (totalEncounterXp) breakdown.push(`${totalEncounterXp.toLocaleString()} XP encounter total`); + if (playerNames.length) breakdown.push(`Players: ${playerNames.join(", ")}`); + if (monsterLines.length) breakdown.push(`Monsters: ${monsterLines.join("; ")}`); + if (monsterLines.length && monsterXpTotal) breakdown.push(`Monster XP total: ${monsterXpTotal.toLocaleString()}`); + if (bonusXp > 0) breakdown.push(`Bonus XP: ${bonusXp.toLocaleString()}`); + + // If raw XP award data exists (from distributor/combat flags), surface it for debugging + if (encounter.xpAwardsRaw) { + const keys = Object.keys(encounter.xpAwardsRaw); + breakdown.push(`XP awards data keys: ${keys.join(", ")}`); + if (encounter.xpAwardsRaw.pf1) { + breakdown.push(`pf1 keys: ${Object.keys(encounter.xpAwardsRaw.pf1).join(", ")}`); + console.log("[GowlersTracking] XP Tooltip build - xpAwardsRaw.pf1 contents:", encounter.xpAwardsRaw.pf1); + } } // Add encounter status @@ -1109,6 +1140,30 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op 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; + + // XP debug logging for data inspection + try { + console.log("[GowlersTracking][XP DEBUG] -----"); + console.log("[GowlersTracking][XP DEBUG] encounterId:", encounterId, "manualXp:", manualXp, "hasExplicitEncounter:", hasExplicitEncounter); + console.log("[GowlersTracking][XP DEBUG] options keys:", Object.keys(options || {})); + console.log("[GowlersTracking][XP DEBUG] change keys:", Object.keys(change || {})); + if (options) console.log("[GowlersTracking][XP DEBUG] options JSON:", JSON.stringify(options, null, 2)); + if (change) console.log("[GowlersTracking][XP DEBUG] change JSON keys:", Object.keys(change)); + if (change?.system?.details?.xp) console.log("[GowlersTracking][XP DEBUG] change.system.details.xp:", change.system.details.xp); + if (change?.system?.attributes?.hp) console.log("[GowlersTracking][XP DEBUG] change.system.attributes.hp (truncated):", JSON.stringify(change.system.attributes.hp, null, 2).substring(0, 500)); + const pf1Flags = actor.getFlag("pf1") ?? {}; + if (pf1Flags && Object.keys(pf1Flags).length) console.log("[GowlersTracking][XP DEBUG] actor pf1 flags keys:", Object.keys(pf1Flags)); + if (game.combat) console.log("[GowlersTracking][XP DEBUG] active combat flags:", game.combat.flags ?? "(none)"); + const encounterFlag = actor.getFlag(FLAG_SCOPE, ENCOUNTER_FLAG) ?? []; + console.log("[GowlersTracking][XP DEBUG] actor encounter flag count:", encounterFlag.length); + if (encounterFlag.length && encounterId) { + const encounterEntry = encounterFlag.find((e) => e.encounterID === encounterId); + console.log("[GowlersTracking][XP DEBUG] matched encounter entry:", encounterEntry ?? "(not found)"); + } + console.log("[GowlersTracking][XP DEBUG] -----"); + } catch (e) { + console.warn("[GowlersTracking][XP DEBUG] logging failed:", e); + } } // Detect source of the change with actor and item details @@ -1779,6 +1834,8 @@ async function updateEncounterSummary(actor, combat, status = "ongoing") { dateCreated: Date.now(), dateUpdated: Date.now(), participants: [], + participantDetails: [], + xpAwardsRaw: null, status: status, xpGained: 0, rounds: 0, @@ -1793,12 +1850,41 @@ async function updateEncounterSummary(actor, combat, status = "ongoing") { // Update participant list with all combatants const participantIds = new Set(); + const participantDetails = []; for (const combatant of combat.combatants) { - if (combatant.actor) { - participantIds.add(combatant.actor.id); + const cActor = combatant.actor; + if (cActor) { + participantIds.add(cActor.id); + participantDetails.push({ + id: cActor.id, + name: cActor.name, + isPlayer: cActor.hasPlayerOwner || cActor.type === "character", + type: cActor.type, + disposition: combatant.token?.disposition, + }); } } encEntry.participants = Array.from(participantIds); + encEntry.participantDetails = participantDetails; + encEntry.xpAwardsRaw = combat.flags ? foundry.utils.duplicate(combat.flags) : null; + if (encEntry.xpAwardsRaw) { + try { + console.log("[GowlersTracking] Captured xpAwardsRaw keys:", Object.keys(encEntry.xpAwardsRaw)); + if (encEntry.xpAwardsRaw.pf1) { + console.log("[GowlersTracking] xpAwardsRaw.pf1 keys:", Object.keys(encEntry.xpAwardsRaw.pf1)); + console.log("[GowlersTracking] xpAwardsRaw.pf1 payload:", encEntry.xpAwardsRaw.pf1); + } + } catch (e) { + console.warn("[GowlersTracking] Failed to log xpAwardsRaw keys:", e); + } + } else { + console.log("[GowlersTracking] No xpAwardsRaw captured on combat flags"); + } + try { + console.log("[GowlersTracking] Combat flags snapshot:", combat.flags ?? "(none)"); + } catch (e) { + console.warn("[GowlersTracking] Failed to log combat flags snapshot:", e); + } // Keep recent encounters (max 50) if (existing.length > 50) {