Planer: Clarity of Corundum + separate Tankbuster-Fixes

- Clarity of Corundum (GNB Proc, 15% DR, statusId 1002684) neu in
  MITIGATION_ABILITIES, ABILITY_DR, JOB_ABILITIES, TIMELINE_PERSONAL_ABILITIES
- statusId fuer Heart of Corundum (1002683) und Great Nebula (1003838)
  nachgetragen (stabilere Erkennung via buffs-Feld)
- Icons heruntergeladen: heart-of-corundum.png, heart-of-stone.png,
  clarity-of-corundum.png (teilt HoC-Action-Icon vorlaeufig)
- Cache-Version v3 -> v4 (erzwingt Neu-Fetch nach Erkennungs-Aenderungen)

Separate Tankbuster:
- tankMaxHpFromEvent(): maxHP direkt aus dem aoe_event des getroffenen
  Tanks statt Roster-Durchschnitt (ein Tank getroffen = sein MaxHP)
- renderMechanicListHtml: m.tankMaxHp hat Vorrang vor avgTankMaxHp(plan)
- plannedAssignmentsForMechanic: persoenliche Mitigation erscheint nur
  auf der direkt zugewiesenen Mechanik (DRK-CDs nicht mehr auf GNB-TB)
- DR-Label: 'mitigiert' -> 'nach DR' (verbleibender Schaden, nicht
  reduzierter Betrag)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xziino 2026-05-24 15:04:21 +02:00
parent 7eeeb5ef56
commit a9b3cc8666
7 changed files with 59 additions and 8 deletions

View File

@ -122,10 +122,11 @@ const MITIGATION_ABILITIES = [
// GNB // GNB
'Superbolide' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 16152], 'Superbolide' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 16152],
'Nebula' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 16148], 'Nebula' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 16148],
'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36935], 'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'statusId' => 1003838, 'extraAbilityGameID' => 36935],
'Camouflage' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 16140], 'Camouflage' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 16140],
'Heart of Stone' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 16161], 'Heart of Stone' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 16161],
'Heart of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 25758], 'Heart of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002683, 'extraAbilityGameID' => 25758],
'Clarity of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002684, 'extraAbilityGameID' => 25758], // Proc von Heart of Corundum, kann beliebiges Partymitglied treffen
// DPS // DPS
'Riddle of Earth' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7394], 'Riddle of Earth' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7394],
'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241], 'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241],

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
const CACHED_LOG_DIR = __DIR__ . '/../cached_logs'; const CACHED_LOG_DIR = __DIR__ . '/../cached_logs';
const CACHED_LOG_VERSION = 'v3'; const CACHED_LOG_VERSION = 'v4';
function cache_language(string $language): string { function cache_language(string $language): string {
$language = strtolower(trim($language)); $language = strtolower(trim($language));

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -69,6 +69,7 @@
{ name: 'Camouflage', buffType: 'buff', extraAbilityGameID: 16140, duration: 20 }, { name: 'Camouflage', buffType: 'buff', extraAbilityGameID: 16140, duration: 20 },
{ name: 'Heart of Stone', buffType: 'buff', extraAbilityGameID: 16161, duration: 7 }, { name: 'Heart of Stone', buffType: 'buff', extraAbilityGameID: 16161, duration: 7 },
{ name: 'Heart of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 8 }, { name: 'Heart of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 8 },
{ name: 'Clarity of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 4 }, // Proc von HoC, geht auf beliebiges Partymitglied
{ name: 'Reprisal', buffType: 'debuff' }, { name: 'Reprisal', buffType: 'debuff' },
], ],
'WHM': [ 'WHM': [
@ -156,7 +157,7 @@
'Oblation': 'DRK', 'Oblation': 'DRK',
'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB', 'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB',
'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': 'GNB', 'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': 'GNB',
'Heart of Corundum': 'GNB', 'Heart of Corundum': 'GNB', 'Clarity of Corundum': 'GNB',
'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM', 'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH', 'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH', 'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
@ -219,6 +220,9 @@
'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png', 'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png',
'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png', 'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png',
'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png', 'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png',
'Heart of Corundum': 'assets/icons/mitigation/heart-of-corundum.png',
'Clarity of Corundum': 'assets/icons/mitigation/clarity-of-corundum.png',
'Heart of Stone': 'assets/icons/mitigation/heart-of-stone.png',
}; };
const ABILITY_DR = { const ABILITY_DR = {
@ -259,6 +263,7 @@
'Camouflage': 0.10, 'Camouflage': 0.10,
'Heart of Stone': 0.15, 'Heart of Stone': 0.15,
'Heart of Corundum': 0.15, 'Heart of Corundum': 0.15,
'Clarity of Corundum': 0.15,
'Riddle of Earth': 0.20, 'Riddle of Earth': 0.20,
'Third Eye': 0.10, 'Third Eye': 0.10,
}; };

View File

@ -493,10 +493,40 @@ function avgNonTankMaxHp(plan) {
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length); return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length);
} }
function avgTankMaxHp(plan) {
const roster = plan.playerRoster ?? [];
const jobComp = plan.jobComposition ?? [];
const hps = jobComp
.map((job, i) => ({ job, maxHp: roster[i]?.maxHp ?? 0 }))
.filter(p => p.job && JOB_ROLE[p.job] === 'tank' && p.maxHp > 0)
.map(p => p.maxHp);
if (!hps.length) return 0;
// Tankbuster trifft einen Tank → Durchschnitt über alle gefundenen Tanks
// (1 Tank → sein MaxHP direkt; 2 Tanks → (hp1 + hp2) / 2)
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length);
}
// Ermittelt den maxHP-Wert direkt aus dem aoe_event (für präzise Tankbuster-Anzeige)
function tankMaxHpFromEvent(ev) {
if (!ev?.isHeavyTankbuster) return 0;
const tankTargets = (ev.targets ?? []).filter(t => t.role === 'tank' && (t.maxHp ?? 0) > 0);
if (tankTargets.length === 1) return tankTargets[0].maxHp;
if (tankTargets.length > 1) {
// Mehrere Tanks gleichzeitig → Durchschnitt
return Math.round(tankTargets.reduce((s, t) => s + t.maxHp, 0) / tankTargets.length);
}
// Fallback: erstes Target mit maxHp (z.B. wenn Role nicht gesetzt)
const any = (ev.targets ?? []).find(t => (t.maxHp ?? 0) > 0);
return any?.maxHp ?? 0;
}
function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) { function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) {
const isTankbuster = !!mechanic.isHeavyTankbuster;
let mult = 1; let mult = 1;
for (const a of assignments) { for (const a of assignments) {
if (a.buffType === 'shield') continue; if (a.buffType === 'shield') continue;
// Persönliche Mitigation nur bei Tankbustern einrechnen
if (!isTankbuster && TIMELINE_PERSONAL_ABILITIES.has(a.ability)) continue;
mult *= (1 - (ABILITY_DR[a.ability] ?? 0)); mult *= (1 - (ABILITY_DR[a.ability] ?? 0));
} }
return mult; return mult;
@ -510,6 +540,13 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) {
for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) { for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) {
if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) continue; if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) continue;
// Persönliche Mitigation zeigt nur auf der Mechanik, der sie direkt zugewiesen ist
// (verhindert dass DRK-Cooldowns auf GNB-Tankbuster erscheinen und umgekehrt)
if (TIMELINE_PERSONAL_ABILITIES.has(entry.assignment.ability)
&& entry.mechanic.id !== targetMechanic.id) {
continue;
}
result.push({ result.push({
...entry.assignment, ...entry.assignment,
sourceMechanicId: entry.mechanic.id, sourceMechanicId: entry.mechanic.id,
@ -535,9 +572,14 @@ function renderMechanicListHtml(plan) {
} }
const activeJobSet = new Set(plan.jobComposition.filter(j => j)); const activeJobSet = new Set(plan.jobComposition.filter(j => j));
const avgHp = avgNonTankMaxHp(plan); const nonTankAvgHp = avgNonTankMaxHp(plan);
const tankAvgHp = avgTankMaxHp(plan);
return mechanics.map(m => { return mechanics.map(m => {
// Tankbuster: gespeicherter maxHP des getroffenen Tanks hat Vorrang (präziser als Roster-Durchschnitt)
const avgHp = m.isHeavyTankbuster
? ((m.tankMaxHp ?? 0) > 0 ? m.tankMaxHp : tankAvgHp)
: nonTankAvgHp;
const planned = plannedAssignmentsForMechanic(plan, m); const planned = plannedAssignmentsForMechanic(plan, m);
const sorted = sortedAssignments(planned); const sorted = sortedAssignments(planned);
const assignHtml = sorted.length === 0 const assignHtml = sorted.length === 0
@ -588,7 +630,7 @@ function renderMechanicListHtml(plan) {
: '' : ''
} }
${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row"> ${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row">
${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> mitigiert` : ''} ${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> nach DR` : ''}
${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''} ${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''}
</div>` : ''} </div>` : ''}
<div class="mechanic-assignments">${assignHtml}</div> <div class="mechanic-assignments">${assignHtml}</div>
@ -711,6 +753,7 @@ const TIMELINE_PERSONAL_ABILITIES = new Set([
'Camouflage', 'Camouflage',
'Heart of Stone', 'Heart of Stone',
'Heart of Corundum', 'Heart of Corundum',
'Clarity of Corundum',
'Divine Benison', 'Divine Benison',
'Intersection', 'Intersection',
'the Spire', 'the Spire',
@ -2267,6 +2310,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
phase: phase?.name ?? '', phase: phase?.name ?? '',
unmitigatedDamage: avgUnmit, unmitigatedDamage: avgUnmit,
isHeavyTankbuster: !!ev.isHeavyTankbuster, isHeavyTankbuster: !!ev.isHeavyTankbuster,
tankMaxHp: tankMaxHpFromEvent(ev),
notes: '', notes: '',
assignments, assignments,
}; };
@ -2387,9 +2431,10 @@ async function refreshPlanLanguage(planId) {
})); }));
return { return {
...mechanic, ...mechanic,
name: match.abilityName ?? mechanic.name, name: match.abilityName ?? mechanic.name,
abilityId: match.abilityId ?? mechanic.abilityId, abilityId: match.abilityId ?? mechanic.abilityId,
isHeavyTankbuster: mechanic.mechanicTypeManual ? !!mechanic.isHeavyTankbuster : !!match.isHeavyTankbuster, isHeavyTankbuster: mechanic.mechanicTypeManual ? !!mechanic.isHeavyTankbuster : !!match.isHeavyTankbuster,
tankMaxHp: mechanic.mechanicTypeManual ? (mechanic.tankMaxHp ?? 0) : tankMaxHpFromEvent(match),
assignments, assignments,
}; };
}); });