Compare commits

...

4 Commits

Author SHA1 Message Date
xziino
76f9baec40 Planer: Phys/Mag-Badge bei Boss-Mechaniken im Gantt-Diagramm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:36:04 +02:00
xziino
bfd40793aa Analyse: sourceID-Tracking für Rampart (statusId 1001191 + Cache v12)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:02:22 +02:00
xziino
4003060699 Analyse+Planer: Phys/Mag-DR fix (Feint/Addle) + Plenary Indulgence (WHM)
- ffxiv-data.js: abilityDr() für typabhängige DR — Feint 10% phys/5% mag, Addle 10% mag/5% phys
- analysis.js + planner.js: abilityDr() in simulateDrMultiplier() und Debuff-Tooltip
- analysis.php: Plenary Indulgence (statusId 1001219 "Confession", 10% DR, WHM)
- ffxiv-data.js: Plenary Indulgence in JOB_ABILITIES/ABILITY_DR/MITIG_ICONS
- Action.json: Eintrag 7433 (recast 600 = 60s) für Gantt-Diagramm
- Icon: plenary-indulgence.png von XIVAPI
- cache.php: v11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 12:53:58 +02:00
xziino
3726a35a72 Analyse+Planer: Phys/Mag-Badge für Schadenstyp (abilityType)
- analysis.php: masterData.abilities.type im GQL-Query + abilityType in AoE-Events
- analysis.js + planner.js: dmgTypeBadge() zeigt [Phys]/[Mag] neben Ability-Namen
- cache.php: Version v8 → v9 (Cache-Invalidierung)
- components.css + planner.css: .dmg-type-badge Styles (orange=phys, blau=mag)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 12:29:52 +02:00
9 changed files with 78 additions and 11 deletions

View File

@ -88,6 +88,7 @@ const MITIGATION_ABILITIES = [
'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001894, 'extraAbilityGameID' => 16471],
'Heart of Light' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001839, 'extraAbilityGameID' => 16160],
'Temperance' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001873, 'extraAbilityGameID' => 16536],
'Plenary Indulgence' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001219, 'extraAbilityGameID' => 7433], // FFLogs: "Confession"
'Aquaveil' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002708, 'extraAbilityGameID' => 25861], // Personal, WHM auf Ziel
'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001944, 'extraAbilityGameID' => 188],
'Expedient' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002711, 'extraAbilityGameID' => 25868], // FFLogs: "Desperate Measures"
@ -101,7 +102,7 @@ const MITIGATION_ABILITIES = [
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826, 'extraAbilityGameID' => 16012],
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707, 'extraAbilityGameID' => 25857],
// ── Personal / targeted mitigation ─────────────────────────────────────
'Rampart' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7531],
'Rampart' => ['dr' => 20, 'buffType' => 'buff', 'statusId' => 1001191, 'extraAbilityGameID' => 7531],
// PLD
'Hallowed Ground' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 30],
'Sentinel' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 17],
@ -251,6 +252,7 @@ $pdResult = fflogs_gql(<<<GQL
abilities {
gameID
name
type
}
}
}
@ -261,10 +263,12 @@ GQL);
if (isset($pdResult['_reauth'])) { echo json_encode(['reauth' => true]); exit; }
if (isset($pdResult['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $pdResult['_curl_error']]); exit; }
// abilityGameID/statusID → display name
// abilityGameID/statusID → display name + damage type
$abilityNames = [];
$abilityTypes = [];
foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) {
$abilityNames[(int)$ab['gameID']] = $ab['name'];
if (isset($ab['type'])) $abilityTypes[(int)$ab['gameID']] = (int)$ab['type'];
}
// gameID → mitigation meta: primary from masterData, statusId fallback for
@ -524,6 +528,7 @@ foreach ($allEvents as $ev) {
'maxHp' => (int)($ev['targetResources']['maxHitPoints'] ?? 0),
'buffs' => $ev['buffs'] ?? '',
'name' => $abilityNames[$abId] ?? $ev['ability']['name'] ?? ('Ability #' . $abId),
'abilityType' => isset($ev['ability']['type']) ? (int)$ev['ability']['type'] : ($abilityTypes[$abId] ?? null),
];
}
@ -543,6 +548,7 @@ foreach ($byAbility as $abId => $events) {
'timestamp' => (int)$ev['ts'],
'abilityId' => $abId,
'abilityName' => $ev['name'],
'abilityType' => $ev['abilityType'] ?? null,
'targets' => [],
];
}
@ -633,6 +639,7 @@ foreach ($clusters as $group) {
'timestamp' => $group['timestamp'],
'abilityId' => $group['abilityId'],
'abilityName' => $group['abilityName'],
'abilityType' => $group['abilityType'] ?? null,
'targets' => $targets,
'totalDamage' => array_sum(array_column($targets, 'amount')),
'isHeavyTankbuster' => $isHeavyTankbuster,

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -419,6 +419,18 @@
"icon": "https://xivapi.com/i/003000/003087_hr1.png",
"shield": null
},
"7433": {
"cast": 0,
"recast": 600,
"names": {
"en": "Plenary Indulgence",
"de": "Vollkommener Ablass",
"fr": "Indulgence plénière",
"jp": "インドゥルゲンティア"
},
"icon": null,
"shield": null
},
"16536": {
"cast": 0,
"recast": 1200,

View File

@ -143,6 +143,10 @@ select option { background: var(--bg2); }
.badge-gold { background: var(--goldbg); border-color: rgba(200,168,75,.4); color: var(--gold); }
.badge-planned { opacity: 0.55; border-style: dashed; }
.dmg-type-badge { font-size: 11px; font-weight: 600; padding: 1px 5px; border-radius: 3px; letter-spacing: .03em; }
.dmg-type--phys { background: rgba(230,140,60,.15); color: var(--orange); border: 1px solid rgba(230,140,60,.35); }
.dmg-type--mag { background: rgba(74,158,255,.12); color: var(--blue); border: 1px solid rgba(74,158,255,.3); }
/* ── DR Bar ─────────────────────────────────────────────────────────────────── */
.dr-bar-wrap {
background: var(--bg2);

View File

@ -276,6 +276,9 @@
}
.mechanic-name {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
color: var(--t1);
font-weight: 500;
@ -917,6 +920,9 @@
position: absolute;
top: 8px;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 150px;
padding: 5px 8px;
border: 1px solid rgba(224,92,92,.35);
@ -926,8 +932,6 @@
font-size: 12px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 3;
}
@ -947,6 +951,15 @@
background: rgba(177,112,255,.28);
}
.timeline-boss-action-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.timeline-boss-action .dmg-type-badge {
flex-shrink: 0;
}
.timeline-mitigation {
position: absolute;
top: 6px;

View File

@ -1,5 +1,12 @@
(function () {
const { MITIG_ICONS, JOB_ABBR, ABILITY_JOBS, JOB_ROLE } = window.FF14_DATA;
const { MITIG_ICONS, JOB_ABBR, ABILITY_JOBS, JOB_ROLE, abilityTypeIsPhysical, abilityTypeIsMagical, abilityDr } = window.FF14_DATA;
function dmgTypeBadge(abilityType) {
if (abilityType == null) return '';
if (abilityTypeIsPhysical(abilityType)) return '<span class="dmg-type-badge dmg-type--phys">Phys</span>';
if (abilityTypeIsMagical(abilityType)) return '<span class="dmg-type-badge dmg-type--mag">Mag</span>';
return '';
}
// Deduplicated list of all mitigations across all targets of a ref event
function collectRefMitigs(refEvent) {
@ -605,7 +612,8 @@
].map(m => {
const iconSrc = mitigationIcon(m);
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
const drPct = Math.round(abilityDr(m.name, ev.abilityType) * 100) || m.dr || 0;
const dr = drPct > 0 ? ` ${drPct}%` : '';
const jobAbbr = m.sourcePlayerType ? (JOB_ABBR[m.sourcePlayerType] ?? '') : '';
const label = jobAbbr ? `${jobAbbr} · ${m.name}` : m.name;
return m.missing
@ -758,6 +766,7 @@
<div>
<div class="aoe-ability">
${ev.abilityName}
${dmgTypeBadge(ev.abilityType)}
<span class="aoe-total"> ${fmtDmg(ev.totalDamage)} total</span>
${debuffIconsHtml}
</div>

View File

@ -10,6 +10,13 @@
function abilityTypeIsPhysical(type) { return (parseInt(type) & ABILITY_TYPE_PHYSICAL) !== 0; }
function abilityTypeIsMagical(type) { return (parseInt(type) & ABILITY_TYPE_MAGICAL) !== 0; }
// Type-aware DR: Feint 10% phys / 5% mag, Addle 10% mag / 5% phys
function abilityDr(abilityName, abilityType) {
if (abilityName === 'Feint') return abilityTypeIsPhysical(abilityType) ? 0.10 : 0.05;
if (abilityName === 'Addle') return abilityTypeIsMagical(abilityType) ? 0.10 : 0.05;
return ABILITY_DR[abilityName] ?? 0;
}
const JOB_FROM_TYPE = {
'Paladin': 'PLD', 'Warrior': 'WAR', 'DarkKnight': 'DRK', 'Gunbreaker': 'GNB',
'WhiteMage': 'WHM', 'Scholar': 'SCH', 'Astrologian': 'AST', 'Sage': 'SGE',
@ -89,6 +96,7 @@
],
'WHM': [
{ name: 'Temperance', buffType: 'buff' },
{ name: 'Plenary Indulgence', buffType: 'buff', extraAbilityGameID: 7433, duration: 10 },
{ name: 'Aquaveil', buffType: 'buff', extraAbilityGameID: 25861, duration: 8 }, // Personal, WHM auf Ziel
{ name: 'Divine Benison', buffType: 'shield', extraAbilityGameID: 7432, duration: 15 },
{ name: 'Divine Caress', buffType: 'shield' },
@ -177,7 +185,7 @@
'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB',
'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': 'GNB',
'Heart of Corundum': 'GNB', 'Clarity of Corundum': 'GNB',
'Temperance': 'WHM', 'Aquaveil': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
'Temperance': 'WHM', 'Plenary Indulgence': 'WHM', 'Aquaveil': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
'Collective Unconscious': 'AST', 'Exaltation': 'AST', 'Neutral Sect': 'AST',
@ -204,6 +212,7 @@
'Dark Missionary': 'assets/icons/mitigation/dark-missionary.png',
'Heart of Light': 'assets/icons/mitigation/heart-of-light.png',
'Temperance': 'assets/icons/mitigation/temperance.png',
'Plenary Indulgence': 'assets/icons/mitigation/plenary-indulgence.png',
'Sacred Soil': 'assets/icons/mitigation/sacred-soil.png',
'Expedient': 'assets/icons/mitigation/expedient.png',
'Fey Illumination': 'assets/icons/mitigation/fey-illumination.png',
@ -258,6 +267,7 @@
'Dark Missionary': 0.10,
'Heart of Light': 0.10,
'Temperance': 0.10,
'Plenary Indulgence': 0.10,
'Sacred Soil': 0.10,
'Expedient': 0.10,
'Collective Unconscious': 0.10,
@ -323,5 +333,6 @@
ABILITY_TYPE_MAGICAL,
abilityTypeIsPhysical,
abilityTypeIsMagical,
abilityDr,
};
})();

View File

@ -651,7 +651,7 @@ function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []
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 - abilityDr(a.ability, mechanic.abilityType));
}
return mult;
}
@ -749,7 +749,7 @@ function renderMechanicListHtml(plan) {
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
<div class="mechanic-body">
${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''}
<div class="mechanic-name">${escHtml(m.name)}</div>
<div class="mechanic-name">${escHtml(m.name)}${dmgTypeBadge(m.abilityType)}</div>
${m.unmitigatedDamage
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert${avgHp ? ` <span class="mechanic-avg-hp">∅ ${fmtNumber(avgHp)} HP</span>` : ''}</div>`
: ''
@ -1258,7 +1258,7 @@ function renderTimelineHtml(plan) {
style="left:${left}%;top:${8 + lane * 30}px"
data-mechanic-id="${escHtml(m.id)}"
title="${escHtml(fmtTimestamp(m.timestamp))} · ${escHtml(m.name)} · ${m.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}">
${escHtml(m.name)}
<span class="timeline-boss-action-name">${escHtml(m.name)}</span>${dmgTypeBadge(m.abilityType)}
</button>
`).join('');
@ -2338,7 +2338,17 @@ const {
MELEE_JOBS,
MITIG_ICONS,
TANK_JOBS,
abilityTypeIsPhysical,
abilityTypeIsMagical,
abilityDr,
} = window.FF14_DATA;
function dmgTypeBadge(abilityType) {
if (abilityType == null) return '';
if (abilityTypeIsPhysical(abilityType)) return '<span class="dmg-type-badge dmg-type--phys">Phys</span>';
if (abilityTypeIsMagical(abilityType)) return '<span class="dmg-type-badge dmg-type--mag">Mag</span>';
return '';
}
// Groups of abilities that are functionally equivalent across different jobs.
// Used to suggest replacements when a job is missing from the composition.
const ABILITY_EQUIVALENTS = [
@ -2442,6 +2452,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
id: crypto.randomUUID(),
name: ev.abilityName,
abilityId: ev.abilityId,
abilityType: ev.abilityType ?? null,
timestamp: relTs,
phase: phase?.name ?? '',
unmitigatedDamage: avgUnmit,