forked from xziino/ff14-mitigator
Compare commits
4 Commits
1e881be482
...
76f9baec40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f9baec40 | ||
|
|
bfd40793aa | ||
|
|
4003060699 | ||
|
|
3726a35a72 |
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
BIN
assets/icons/mitigation/plenary-indulgence.png
Normal file
BIN
assets/icons/mitigation/plenary-indulgence.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user