zwischenstand

This commit is contained in:
centron\schwoerer
2025-11-18 14:32:01 +01:00
parent e553e809ae
commit 4f0b3af6e1
5 changed files with 1721 additions and 574 deletions

View File

@@ -1,28 +1,84 @@
<form class="tracking-ledger-config">
<section class="tracking-ledger-controls">
<label class="tracking-ledger-filter">
Filter actors:
<input type="text" data-filter-input placeholder="Type a name..." value="{{filter}}" autocomplete="off" autofocus>
</label>
<label class="tracking-ledger-page-size">
Rows per page:
<select data-page-size>
{{#each pageOptions}}
<option value="{{value}}" {{#if selected}}selected{{/if}}>{{value}}</option>
{{/each}}
</select>
</label>
<div class="tracking-ledger-pagination">
<button type="button" data-action="page" data-direction="prev" {{#unless hasPrev}}disabled{{/unless}}>&laquo; Prev</button>
{{#if totalActors}}
<span>Page {{displayPage}} / {{totalPages}}</span>
{{else}}
<span>Page 0 / 0</span>
{{/if}}
<button type="button" data-action="page" data-direction="next" {{#unless hasNext}}disabled{{/unless}}>Next &raquo;</button>
</div>
<div class="tracking-ledger-summary">
{{#if totalActors}}
Showing {{showingFrom}}-{{showingTo}} of {{totalActors}} actors
{{else}}
No actors found.
{{/if}}
</div>
</section>
<table class="tracking-ledger-table">
<thead>
<tr>
<th>Actor</th>
<th>HP</th>
<th>XP</th>
<th>Currency</th>
<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>
</tr>
</thead>
<tbody>
{{#each actors}}
<tr>
<td>{{name}}</td>
<td>
<input type="checkbox" name="actors.{{id}}.hp" {{#if tracking.hp}}checked{{/if}}>
</td>
<td>
<input type="checkbox" name="actors.{{id}}.xp" {{#if tracking.xp}}checked{{/if}}>
</td>
<td>
<input type="checkbox" name="actors.{{id}}.currency" {{#if tracking.currency}}checked{{/if}}>
</td>
</tr>
{{/each}}
{{#if actors.length}}
{{#each actors}}
<tr>
<td>
{{name}}
<input type="hidden" name="actors.{{id}}.__present" value="1">
</td>
<td>
<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}}>
</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}}>
</td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="7" class="empty">No actors match the current filter.</td>
</tr>
{{/if}}
</tbody>
</table>
<footer>

View File

@@ -0,0 +1,322 @@
(async () => {
/*
* Activate damage tracking meter for encounters - similar to WoW Damage Meter
* Tracks total damage dealt by each actor during active combat
* Stores data in actor flags for persistence during encounter
* Optimized for PF1e damage roll detection
*/
const FLAG_SCOPE = "world";
const FLAG_KEY = "pf1DamageHistory";
const MODULE_ID = "pf1-damage-meter";
const MAX_DAMAGE_RECORDS = 200;
// Initialize global state
game.pf1 ??= {};
game.pf1.damageMeter ??= {
state: {}, // actorId -> { totalDamage, damageLog: [] }
hooks: {}, // Store hook IDs for cleanup
currentCombat: null, // Current active combat ID
isActive: false
};
const meterState = game.pf1.damageMeter;
const logKey = `pf1-damage-meter`;
// Check if already active - toggle off
if (meterState.hooks[logKey]) {
Hooks.off("createChatMessage", meterState.hooks[logKey].createMessage);
Hooks.off("combatStart", meterState.hooks[logKey].combatStart);
Hooks.off("combatEnd", meterState.hooks[logKey].combatEnd);
delete meterState.hooks[logKey];
meterState.isActive = false;
ui.notifications.info("Damage meter tracking disabled.");
return;
}
// Enable damage meter
meterState.hooks[logKey] = {};
/**
* Extract damage from PF1e attack metadata
* Handles both normal and critical damage
*/
function extractPF1eDamage(chatMessage) {
try {
const metadata = chatMessage.flags?.pf1?.metadata?.rolls?.attacks;
if (!metadata || !Array.isArray(metadata)) return 0;
let totalDamage = 0;
// Sum all damage from all attacks in this message
metadata.forEach(attack => {
// Normal damage rolls
if (Array.isArray(attack.damage)) {
attack.damage.forEach(roll => {
totalDamage += roll.total || 0;
});
}
// Critical damage rolls (in addition to normal)
if (Array.isArray(attack.critDamage)) {
attack.critDamage.forEach(roll => {
totalDamage += roll.total || 0;
});
}
});
return totalDamage;
} catch (err) {
console.warn("[Damage Meter] Error extracting PF1e damage:", err);
return 0;
}
}
/**
* Hook into chat message creation to capture damage rolls
*/
meterState.hooks[logKey].createMessage = Hooks.on(
"createChatMessage",
async (chatMessage, options, userId) => {
// Skip if no active combat
if (!game.combat?.active) return;
// Get the actor who made this roll
const actor = chatMessage.actor;
if (!actor) return;
// Try PF1e damage detection first
const damageTotal = extractPF1eDamage(chatMessage);
// Fallback: check for other roll metadata
if (damageTotal === 0 && chatMessage.rolls?.length > 0) {
const roll = chatMessage.rolls[0];
if (
roll.formula &&
/damage|harmful|d\d+/i.test(roll.formula) &&
roll.total > 0
) {
// Potential damage roll but not PF1e format
// Only track if it looks like damage (has explicit "damage" keyword)
if (/damage/i.test(roll.formula)) {
const potential = roll.total;
if (potential > 0) {
recordDamage(actor, potential, roll.formula);
}
}
}
return;
}
if (damageTotal > 0) {
recordDamage(actor, damageTotal, "PF1e Attack");
}
}
);
/**
* Record damage for an actor
*/
async function recordDamage(actor, damageTotal, source = "Unknown") {
// Initialize actor damage state if needed
if (!meterState.state[actor.id]) {
meterState.state[actor.id] = {
actorName: actor.name,
totalDamage: 0,
damageLog: []
};
}
const actorData = meterState.state[actor.id];
actorData.totalDamage += damageTotal;
// Add entry to damage log
actorData.damageLog.unshift({
timestamp: Date.now(),
damage: damageTotal,
source: source,
roundNumber: game.combat.round
});
// Keep log size manageable
if (actorData.damageLog.length > MAX_DAMAGE_RECORDS) {
actorData.damageLog = actorData.damageLog.slice(0, MAX_DAMAGE_RECORDS);
}
// Persist to actor flag
await persistDamageData(actor, actorData);
}
/**
* Reset damage tracking when combat starts
*/
meterState.hooks[logKey].combatStart = Hooks.on("combatStart", (combat) => {
meterState.currentCombat = combat.id;
meterState.state = {}; // Reset for new encounter
ui.notifications.info("Damage meter tracking started for new encounter.");
});
/**
* Save damage stats when combat ends
*/
meterState.hooks[logKey].combatEnd = Hooks.on(
"combatEnd",
async (combat) => {
const damageReport = formatDamageReport(meterState.state);
// Persist final damage data to chat
if (Object.keys(meterState.state).length > 0) {
await ChatMessage.create({
content: damageReport,
flags: { [MODULE_ID]: { combatId: combat.id } }
});
// Save damage stats to actors
for (const [actorId, data] of Object.entries(meterState.state)) {
const actor = game.actors.get(actorId);
if (actor) {
await persistDamageData(actor, data);
}
}
}
meterState.state = {};
ui.notifications.info("Damage meter encounter ended. Stats saved to chat.");
}
);
meterState.isActive = true;
/**
* Persist damage data to actor flags
*/
async function persistDamageData(actor, data) {
try {
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
// Add new encounter entry
existing.unshift({
timestamp: Date.now(),
totalDamage: data.totalDamage,
damageCount: data.damageLog.length,
combatId: game.combat?.id || "debug",
roundNumber: game.combat?.round || 0
});
// Keep recent encounters
if (existing.length > 50) {
existing.splice(50);
}
await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing);
} catch (err) {
console.error("Damage Meter | Failed to persist data:", err);
}
}
/**
* Format damage data for display
*/
function formatDamageReport(state) {
if (Object.keys(state).length === 0) {
return "<p>No damage data recorded.</p>";
}
// Sort by total damage (descending)
const sorted = Object.values(state).sort(
(a, b) => b.totalDamage - a.totalDamage
);
const totalTeamDamage = sorted.reduce(
(sum, actor) => sum + actor.totalDamage,
0
);
let html = `
<section style="padding: 10px; background: #222; border-radius: 5px; color: #fff; font-family: monospace;">
<h3 style="margin: 0 0 10px 0; color: #aaa; font-size: 14px;">⚔️ Damage Meter Report</h3>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr style="border-bottom: 2px solid #555;">
<th style="text-align: left; padding: 5px;">Actor</th>
<th style="text-align: right; padding: 5px;">Damage</th>
<th style="text-align: right; padding: 5px;">Hits</th>
<th style="text-align: right; padding: 5px;">Avg/Hit</th>
<th style="text-align: right; padding: 5px;">% Total</th>
</tr>
</thead>
<tbody>
`;
sorted.forEach((actor, index) => {
const avgDamage =
actor.damageLog.length > 0
? Math.round(actor.totalDamage / actor.damageLog.length)
: 0;
const percentage =
totalTeamDamage > 0
? Math.round((actor.totalDamage / totalTeamDamage) * 100)
: 0;
const rowColor = index % 2 === 0 ? "#2a2a2a" : "#1f1f1f";
const rankColor = index === 0 ? "#ffd700" : "#ccc"; // Gold for #1
html += `
<tr style="background: ${rowColor}; border-bottom: 1px solid #444;">
<td style="padding: 5px; color: ${rankColor};">${
index + 1
}. ${actor.actorName}</td>
<td style="text-align: right; padding: 5px; color: #ff6b6b;">${
actor.totalDamage
}</td>
<td style="text-align: right; padding: 5px;">${
actor.damageLog.length
}</td>
<td style="text-align: right; padding: 5px;">${avgDamage}</td>
<td style="text-align: right; padding: 5px; color: #4ecdc4;">${percentage}%</td>
</tr>
`;
});
html += `
</tbody>
</table>
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #555; text-align: right; color: #aaa; font-size: 11px;">
Total Team Damage: <span style="color: #ff6b6b; font-weight: bold;">${totalTeamDamage}</span>
</div>
</section>
`;
return html;
}
/**
* Show current damage meter in UI
*/
function showDamageMeter() {
const content = formatDamageReport(meterState.state);
new Dialog({
title: "Damage Meter - Current Encounter",
content: content,
buttons: {
close: {
icon: '<i class="fas fa-times"></i>',
label: "Close"
}
},
default: "close",
width: 500
}).render(true);
}
// Provide public access
game.pf1.showDamageMeter = showDamageMeter;
const statusMessage = meterState.isActive
? `Damage meter tracking ACTIVE. Use 'game.pf1.showDamageMeter()' to view current stats.`
: `Damage meter tracking DISABLED.`;
ui.notifications.info(statusMessage);
console.log("[Damage Meter]", statusMessage);
})();

View File

@@ -0,0 +1,79 @@
(async () => {
/*
* Reset damage meter - disables tracking and clears all damage data
* Removes all damage history flags from all actors
* Clears in-memory tracking state
*/
const FLAG_SCOPE = "world";
const FLAG_KEY = "pf1DamageHistory";
const logKey = `pf1-damage-meter`;
console.log("[Damage Meter Reset] Starting cleanup...");
// Get the damage meter state
const meterState = game.pf1?.damageMeter;
if (!meterState) {
ui.notifications.warn(
"Damage meter was not initialized. Nothing to reset."
);
return;
}
// Disable hooks
if (meterState.hooks[logKey]) {
try {
Hooks.off("createChatMessage", meterState.hooks[logKey].createMessage);
Hooks.off("combatStart", meterState.hooks[logKey].combatStart);
Hooks.off("combatEnd", meterState.hooks[logKey].combatEnd);
delete meterState.hooks[logKey];
console.log("[Damage Meter Reset] Hooks disabled");
} catch (err) {
console.error("[Damage Meter Reset] Error disabling hooks:", err);
}
}
// Clear in-memory state
meterState.state = {};
meterState.currentCombat = null;
meterState.isActive = false;
console.log("[Damage Meter Reset] In-memory state cleared");
// Clear damage history from all actors
let actorsCleared = 0;
const errors = [];
for (const actor of game.actors) {
try {
const hasFlag = await actor.getFlag(FLAG_SCOPE, FLAG_KEY);
if (hasFlag) {
await actor.unsetFlag(FLAG_SCOPE, FLAG_KEY);
actorsCleared++;
console.log(`[Damage Meter Reset] Cleared ${FLAG_KEY} from ${actor.name}`);
}
} catch (err) {
const errorMsg = `Failed to clear ${actor.name}: ${err.message}`;
console.error(`[Damage Meter Reset] ${errorMsg}`);
errors.push(errorMsg);
}
}
// Report results
let message = `✓ Damage meter reset complete!\n`;
message += `• Hooks disabled\n`;
message += `• In-memory state cleared\n`;
message += `• Damage history removed from ${actorsCleared} actor(s)`;
if (errors.length > 0) {
message += `\n\n⚠️ Errors occurred:\n`;
errors.forEach((err) => {
message += `${err}\n`;
});
}
console.log("[Damage Meter Reset] Completed:", message);
ui.notifications.info(message);
})();