ff14-mitigator/CLAUDE.md
xziino d6dedd1a2e Add Cooldown Planner concept and roadmap to CLAUDE.md
Documents data model, import flow with merge logic, feature roadmap,
job-to-ability mapping, recast times, and technical decisions for the
upcoming third tab (Planer).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:26:49 +02:00

19 KiB
Raw Blame History

ff14-mitigator — FFLogs Mitigation Analyzer

Projekt

PHP/HTML/JS-Tool zum Analysieren von FFXIV-Raidlogs via FFLogs OAuth2 PKCE + GraphQL API. Kein Framework, kein Composer, kein npm — Plain PHP für Shared Hosting.

Drei Tabs:

  • Report-Tab: Report-Code eingeben, Fight auswählen → Fight-JSON-Ausgabe + Event Explorer
  • Analyse-Tab: Spielerübersicht + AoE-Timeline mit Mitigation-Tracking, Pull-Vergleich, Phase-Filter
  • Planer-Tab (in Entwicklung): Cooldown-Planer für Raid-Mitigation — Log-Import, manuelle Bearbeitung, Job-basierte Spell-Verfügbarkeit, DR-Simulation

Architektur & Konventionen

Trennung von PHP, HTML und JS

  • PHP-Logik gehört ausschließlich in index.php (und API/Auth-Endpunkte). Keine Geschäftslogik in Templates.
  • HTML gehört in templates/. Jede logisch in sich geschlossene Komponente ist eine eigene Datei.
  • CSS gehört in css/. Jede CSS-Datei hat einen klar abgegrenzten Scope (base, layout, components, analysis).
  • JavaScript gehört in js/. Keine Inline-Scripts in Templates außer dem <script src="..."> Tag in page.php.

Template-System

index.php setzt alle Variablen und ruft dann require templates/page.php auf. Templates sind reine Ausgabe — sie lesen Variablen aus dem Scope, setzen aber keine.

Geteilter JS-State

window.App in app.js hält den gemeinsamen State für alle Tabs:

window.App = { reportCode, fightId, fightStart, fightEnd, phases: [], fights: [] }

window.analysisTab (definiert in analysis.js) stellt Hooks bereit:

  • onFightSelected() — wird von app.js aufgerufen wenn ein Fight gewählt wird
  • onTabOpen() — wird von tabs.js aufgerufen wenn der Analyse-Tab geöffnet wird
  • onFightsLoaded(fights) — wird von app.js aufgerufen nach Report-Load (befüllt Vergleichs-Dropdown)
  • reset() — wird von app.js aufgerufen wenn ein neuer Report geladen wird

Dateistruktur

index.php                   — PHP-Logik: Auth-Check, Variablen, require page.php
config.php                  — Konstanten (CLIENT_ID, URIs) + session_start_safe()
templates/
  page.php                  — HTML-Skeleton (head, body), routet zu login oder app
  login.php                 — Login-Overlay (nicht authentifiziert / Token abgelaufen)
  topbar.php                — Topbar mit Logo + Tab-Navigation + Token-Ablaufzeit
  tab-report.php            — Report-Tab: includes report-form, fight-select, event-explorer, output-card
  tab-analysis.php          — Analyse-Tab: Spieler-Grid + Pull-Vergleich + AoE-Timeline HTML
  report-form.php           — Report-Code-Eingabe Card
  fight-select.php          — Fight-Auswahl Dropdown Card
  event-explorer.php        — Event Explorer Card (Ability/DataType/EventType/Spieler-Filter)
  output-card.php           — Terminal-Ausgabe Card + Initial-Hint
css/
  base.css                  — CSS-Variablen, Reset, Basis-Styles, Feedback-Klassen
  layout.css                — App-Shell, Topbar, Tabs, Login-Overlay, Form-Helpers
  components.css            — Cards, Inputs, Buttons, Badges, Terminal
  analysis.css              — Spieler-Grid, AoE-Timeline, Mitigation-Icons, HP-Bar, Ref-Row
js/
  app.js                    — Formular, Fight-Dropdown, Fetch, window.App State, Event Explorer
  tabs.js                   — Tab-Switching, ruft window.analysisTab.onTabOpen() auf
  analysis.js               — Analyse-Tab: Daten laden, Spieler rendern, Timeline rendern, Pull-Vergleich
auth/
  start.php                 — PKCE generieren, Session speichern, Redirect zu FFLogs
  callback.php              — Code gegen Token tauschen, Token in Session speichern
api/
  fight.php                 — POST-Endpunkt: Fight-Liste via GraphQL → JSON
  analysis.php              — POST-Endpunkt: Spieler + AoE-Events + Mitigations → JSON
  abilities.php             — POST-Endpunkt: Ability- + Spielerliste für Event Explorer Dropdowns
  debug-events.php          — POST-Endpunkt: Raw Events für Event Explorer (mit Filterung)
assets/
  icons/mitigation/         — Lokal gespeicherte Ability-Icons (PNG, von XIVAPI)
debug/
  schema.php                — Einmaliges Schema-Explorer Tool (nicht produktiv deployen)

Design-System

CSS-Variablen in css/base.css:

  • Hintergründe: --bg0 (#08090d) bis --bg3 (#1c2130), --bgcard (#121620)
  • Akzentfarbe: --gold (#c8a84b)
  • Text: --t1 (hell) / --t2 (gedimmt) / --t3 (sehr gedimmt)
  • Farben: --blue, --green, --red, --orange
  • Fonts: --font-d Cinzel (Titel/Logo), --font-b Inter (Body)
  • Border-Radius: --r (klein), --rl (groß)

Analyse-Tab — Konzepte & Entscheidungen

AoE-Erkennung

  • Verwendet damage Events mit includeResources: true (enthält buffs-Feld für Mitigations)
  • Tick-Schaden (ev['tick'] = true) und abilityGameID ≤ 7 (Auto-Attacks) werden gefiltert
  • Proximity Clustering statt fixer Zeitfenster: Events werden pro abilityGameID gesammelt, dann in Cluster eingeteilt — ein neuer Cluster beginnt wenn der Abstand zum ersten Event im aktuellen Cluster > CLUSTER_WINDOW_MS (1000ms) beträgt
  • AoE = Cluster mit ≥ 3 unterschiedlichen targetIDs
  • Pro Target werden amount, absorbed, overkill, hp (nach Treffer), maxHp und buffs gespeichert; bei mehreren Hits desselben Targets pro Cluster werden amount/absorbed/overkill summiert

Spielernamen statt IDs

  • masterData.abilities (gameID → Name) wird zusammen mit playerDetails in einem einzigen Query abgerufen
  • $mitigIdMap (gameID → Mitigation-Meta) wird aus zwei Quellen befüllt:
    1. masterData.abilities — für Abilities die als Events im Report auftauchen (Name-basiertes Matching)
    2. Statische statusId-Felder in MITIGATION_ABILITIES — Fallback für Abilities die nicht in masterData stehen (z.B. Pre-Pull-Buffs)

Mitigation-Tracking

Getrackte party-wide Buffs + Schilde + Boss-Debuffs (definiert in MITIGATION_ABILITIES in api/analysis.php):

Drei buffType-Kategorien:

  • buff: Damage Reduction — Passage of Arms (15%), Troubadour/Tactician/Shield Samba (15%), Dark Missionary/Heart of Light/Temperance/Sacred Soil/Expedient/Collective Unconscious/Holos/Kerachole/Magick Barrier (10%), Fey Illumination (5%)
  • shield: Barrieren — Divine Veil, Guardian, Shake It Off, Bloodwhetting, Divine Benison, Divine Caress, Intersection, Neutral Sect, the Spire, Panhaima, Holosakos, Eukrasian Prognosis, Eukrasian Diagnosis, Differential Diagnosis, Haima, Galvanize, Seraphic Veil, Catalyze, Radiant Aegis, Tempera Coat, Tempera Grassa, Improvised Finish
  • debuff: Boss-Debuffs — Reprisal, Feint, Addle (je 10%)

Statische Status-IDs (statusId-Feld): Jeder Eintrag in MITIGATION_ABILITIES trägt ein statusId-Feld (FFLogs Status-ID = XIVAPI Status row_id + 1.000.000). Diese IDs werden als Fallback in $mitigIdMap eingetragen wenn masterData den Eintrag nicht enthält. Löst das Pre-Pull-Problem und Name-Mismatches (z.B. FFLogs "Guardian's Will" vs. Key 'Guardian', "Desperate Measures" vs. 'Expedient').

Primär: buffs-Feld im damage-Event: Das Feld enthält einen .-separierten String aktiver Status-IDs. resolveMitigations() mappt diese via $mitigIdMap. Doppelte Namen werden dedupliziert.

Fallback: Shield-Timeline via applybuff/removebuff: Das buffs-Feld des damage-Events spiegelt den Zustand nach dem Hit — Schilde die durch den Hit konsumiert wurden fehlen darin. Daher wird in Step 2b ein separater Buffs-Query (dataType: Buffs) für alle Shield-StatusIds gefetcht und eine Timeline aufgebaut: $shieldTimeline[targetId][statusId][] = ['apply' => ts, 'remove' => ts|null]. Bei absorbed > 0 wird shieldsActiveAt() konsultiert und alle dort gefundenen Schilde die noch nicht in der Mitigation-Liste stehen werden hinzugefügt (Name-Deduplication). Buffer: 200ms (remove >= damageTs - 200) um consumed-at-impact von natural-expiry zu unterscheiden.

Mitigation-Icons

  • Icons lokal gespeichert in assets/icons/mitigation/ als PNG
  • Quelle: XIVAPI v2 (https://v2.xivapi.com/api/asset?path=...&format=png)
  • Icon-Pfade per Action-Row-ID abgerufen: https://v2.xivapi.com/api/sheet/Action/{id}?fields=Name,Icon
  • Dateinamen: kebab-case des Ability-Namens (z.B. passage-of-arms.png)
  • Mapping in analysis.js: MITIG_ICONS Objekt (Ability-Name → lokaler Pfad)

Darstellungsort nach buffType:

  • Buffs (buffType: 'buff'): erscheinen per Spieler unter der Spieler-Box (.aoe-target-buffs)
  • Schilde (buffType: 'shield'): erscheinen als Tooltip auf dem +absorbed-Wert (.aoe-target-absorbed) — Liste der aktiven Shield-Namen, consumed Schilde werden über die Timeline-Fallback ergänzt
  • Debuffs (buffType: 'debuff'): erscheinen einmal pro Event im Ability-Header neben "X total" bzw. in der REF-Zeile neben "REF X total ±delta"
  • Fehlende Mitigations aus dem Ref-Pull: Buffs → ausgegraut per Spieler; Debuffs → ausgegraut im jeweiligen Header
  • .aoe-ability nutzt display: flex; align-items: center; gap: 6px — identisches Spacing wie .aoe-ref-label

Implementierte Features (Übersicht neuerer Commits)

HP-Balken pro Spieler

2-Segment-Balken direkt unter Name+Schaden in der Spieler-Box:

  • Grün (links): HP nach dem Treffer — Farbton dynamisch: >50% grün, 2550% amber, <25% rot
  • Rot (Mitte): erlittener Schaden (als % von MaxHP)
  • Nur gerendert wenn t.maxHp > 0; Daten kommen aus targetResources.hitPoints / maxHitPoints via includeResources: true

Death-Highlight + Absorbed/Overkill

  • Spieler-Box bekommt Klasse .aoe-target--dead wenn hp === 0 && maxHp > 0
  • Overkill-Betrag wird links neben dem Job-Badge angezeigt (.aoe-target-overkill)
  • Absorbed-Betrag erscheint als gedimmter Zusatz neben dem Schaden (.aoe-target-absorbed)

Phase-Filter

  • Liest phaseTransitions aus dem gewählten Fight-Objekt (window.App.phases)
  • Dropdown #phase-select erscheint nur wenn Phasen vorhanden sind
  • Option "Ganzer Fight" (id=0) ist immer dabei; individuelle Phasen ab id=1
  • phaseFilter = { startTime, endTime } filtert die Timeline in renderTimeline()

Spieler-Sortierung

Konsistentes Healer → DPS → Tank-Ordering überall: im Spieler-Grid, in jedem AoE-Event, und in der Referenz-Zeile. Innerhalb gleicher Rolle alphabetisch nach Name.

Pull-Vergleich (innerhalb desselben Reports)

  • Dropdown #ref-fight-select wird nach Report-Load mit allen Fights befüllt (via onFightsLoaded)
  • Bei Auswahl: separater api/analysis.php-Call für den Ref-Fight → refEvents[]
  • In renderTimeline(): per abilityName und Occurrence-Index gematchter Ref-Event wird als REF-Zeile unterhalb der aktuellen Targets gerendert
  • Fehlende Mitigations (vorhanden im Ref, nicht im aktuellen Pull) werden als ausgegrautem Icon mit Klasse .aoe-buff-missing gezeigt — Buffs per Spieler, Debuffs im Ability-Header
  • Schaden-Delta pro Spieler: grün wenn besser (aoe-delta-better), rot wenn schlechter (aoe-delta-worse)
  • Gesamt-Delta + Ref-Debuff-Icons in der REF-Headerzeile (aoe-ref-label)

Cross-Report-Vergleich

  • Button "+ Anderer Report" (#ref-ext-toggle) öffnet Panel mit Eingabefeld + Laden-Button
  • api/fight.php wird mit dem externen Report-Code aufgerufen → Fight-Dropdown befüllt
  • Auswahl eines externen Fights → api/analysis.php-Call mit externem Code → refEvents[]
  • Same-Report- und Cross-Report-Selektion schließen sich gegenseitig aus (jeweils Reset des anderen Dropdowns)

Event Explorer (im Report-Tab)

Erscheint nach Fight-Auswahl als Card über dem Output-Terminal. Filteroptionen:

  • Ability: Dropdown mit allen NPC-Fähigkeiten des Fights (via api/abilities.php)
  • DataType: DamageTaken / DamageDone / Healing / Casts / Buffs / Deaths
  • Event-Typ (raw): damage, calculateddamage, applybuff, removebuff, etc.
  • Spieler: Dropdown mit allen Spielern des Fights (via api/abilities.php)
  • Limit: 1500 Events
  • Von/Bis: Zeitoffsets in Sekunden vom Fight-Start
  • Output landet im gleichen #output-Terminal wie der Fight-JSON
  • Backend api/debug-events.php: filtert Events nach Typ, Ability-ID und Spieler (Source oder Target); gibt Metadaten + gefilterte Events zurück

Konfiguration

  • config.php: CLIENT_ID, REDIRECT_URI, DEV_MODE anpassen
  • DEV_MODE = true deaktiviert SSL-Verifizierung (nur lokal, nie in Produktion)
  • session.cookie_secure ist bei DEV_MODE automatisch false

FFLogs API

  • OAuth2 PKCE (kein Client Secret, öffentliche App)
  • App registrieren: https://www.fflogs.com/api/clients/
  • GraphQL Endpoint (user-scoped): https://www.fflogs.com/api/v2/user
  • Token Endpoint: https://www.fflogs.com/oauth/token
  • Kein Refresh Token für öffentliche Clients — abgelaufene Sessions starten PKCE neu
  • Event-Typen: damage (post-Mitigation Snapshot, enthält buffs-Feld) vs. calculateddamage (Snapshot beim Cast-Start — buffs sind zu früh, aber targetResources.absorb zeigt den Schildwert zu diesem Zeitpunkt)
  • buffs-Feld im damage-Event: .-separierter String aktiver Status-IDs. Achtung: Schilde die durch diesen Hit konsumiert wurden sind bereits entfernt → Shield-Timeline als Fallback nötig
  • FFLogs Status-IDs = XIVAPI Status-Sheet row_id + 1.000.000 (z.B. Galvanize: row_id 297 → FFLogs 1000297)
  • dataType: Buffs liefert applybuff/removebuff-Events (u.a. für die Shield-Timeline)
  • includeResources: true im events()-Query liefert targetResources.hitPoints / maxHitPoints pro Event

XIVAPI

  • Basis-URL: https://v2.xivapi.com
  • Action-Lookup per Row-ID: /api/sheet/Action/{id}?fields=Name,Icon
  • Asset-Download: /api/asset?path={tex_path}&format=png
  • Icons nicht hotlinken — lokal speichern (Community-Service, kein SLA)
  • XIVAPI-Suche (/api/search) gibt bei manchen Abilities ClassJob-Daten statt Action-Daten zurück → direkt per Row-ID abrufen

Repository

  • Remote: https://git.epow0.org/xziino/ff14-mitigator
  • Platform: Gitea (git.epow0.org)
  • Branch: main

Lokale Entwicklung

php -S localhost:8080

Dann http://localhost:8080 im Browser öffnen. Redirect URI in FFLogs App und config.php: http://localhost:8080/auth/callback.php

Bekannte Schema-Infos (ReportFight)

Verfügbare aber noch nicht genutzte Felder: friendlyPlayers, enemyNPCs, lastPhase, standardComposition, hasEcho, combatTime Genutzt: phaseTransitions (für Phase-Filter), fightPercentage, kill, startTime, endTime Vollständiges Schema: siehe debug/schema.php oder fflogs-schema.json

Deployment

  • DEV_MODE auf false setzen
  • REDIRECT_URI auf produktive HTTPS-URL anpassen
  • debug/ Ordner nicht deployen
  • assets/ Ordner deployen (enthält lokale Icons)

Planer-Tab — Konzept & Roadmap

Ziel

Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik eingesetzt? Basierend auf Log-Daten oder manuell aufgebaut. Überlebt Browser-Neustarts via localStorage.

Datenmodell (Plan)

{
  "id": "uuid",
  "name": "M8S  Prog Week 1",
  "createdAt": 1234567890,
  "updatedAt": 1234567890,
  "source": { "reportCode": "abc123", "fightId": 6 },
  "jobComposition": ["PLD", "WAR", "WHM", "SCH", "MNK", "DRG", "BRD", "SMN"],
  "mechanics": [
    {
      "id": "uuid",
      "name": "Fourth-Wall Fusion",
      "timestamp": 83000,
      "unmitigatedDamage": 280000,
      "assignments": [
        { "ability": "Reprisal",     "job": "PLD" },
        { "ability": "Shield Samba", "job": "BRD" }
      ]
    }
  ]
}

Mehrere Pläne gespeichert in localStorage unter ff14-planner-plans als Array.

Import-Flow (erster Meilenstein)

Ziel: Einen bestehenden Log als saubere Mechanik-Vorlage laden — ohne vorhandene Mechaniken zu überschreiben.

  1. Benutzer wählt Report-Code + Kampf (gleicher Flow wie im Report-Tab, eigenes Mini-Formular im Planer)
  2. api/analysis.php wird aufgerufen → liefert aoe_events mit Name, Timestamp, unmitigatedAmount
  3. Jedes AoE-Event wird als Mechanik-Kandidat angezeigt (Name + Timestamp + Rohschaden)
  4. Benutzer kann einzelne Events auswählen oder alle übernehmen
  5. Merge-Logik: Beim Import in einen bestehenden Plan werden nur Mechaniken hinzugefügt die noch nicht vorhanden sind — Matching per abilityName + Timestamp-Nähe (± 5s). Bestehende Assignments bleiben erhalten.
  6. Neue Mechaniken werden an der richtigen Timestamp-Position eingefügt (Timeline bleibt sortiert)

Warum Merge statt Überschreiben: Progress-Szenario — erster Import enthält Phase 1, späterer Import (weiter im Fight) fügt Phase 2 hinzu ohne Phase-1-Planung zu verlieren.

Geplante Features (Übersicht)

Prio Feature Beschreibung
1 Import-Flow Log → Mechanik-Timeline, Merge bei Teilimporten
2 Jobaufstellung 8 Slots (2 Tank, 2 Healer, 4 DPS), Auswahl bestimmt verfügbare Spells
3 Cooldown-Zuweisung Pro Mechanik Abilities zuweisen/entfernen per Klick
4 DR-Simulation simuliert = unmitigated × ∏(1 dr_i) live berechnet beim Toggle
5 Recast-Tracking Recast-Datenbank pro Ability; Konflikt-Warnung wenn CD noch läuft
6 Coverage-Ansicht Gantt-Chart: Mechaniken auf X-Achse, Buff-Dauer als Balken
7 Analyse-Overlay Planer-Tab: Vergleich geplanter vs. tatsächlich genutzter CDs (Job-basiertes Matching, nicht Spielername)
8 Shield-Schätzung Empirisch aus Log-Durchschnitt (absorbed-Werte), nicht exakt
9 JSON-Export/Import Plan als Datei teilen mit Raidkollegen

Spell-Verfügbarkeit nach Job

Jobaufstellung → verfügbare Abilities (Subset von MITIGATION_ABILITIES):

Job Abilities
PLD Passage of Arms, Divine Veil, Guardian, Reprisal
WAR Shake It Off, Bloodwhetting, Reprisal
DRK Dark Missionary, Reprisal
GNB Heart of Stone (noch nicht getrackt), Reprisal
WHM Temperance, Divine Benison, Divine Caress
SCH Sacred Soil, Expedient, Fey Illumination, Galvanize, Seraphic Veil, Catalyze, Addle
AST Collective Unconscious, Neutral Sect, Intersection, the Spire
SGE Kerachole, Holos, Holosakos, Panhaima, Eukrasian Prognosis, Eukrasian Diagnosis, Haima, Addle
BRD Troubadour
MCH Tactician
DNC Shield Samba, Improvised Finish
MNK Feint
DRG Feint
NIN Feint
SAM Feint
RPR Feint
VPR Feint
BLM Addle
SMN Addle, Radiant Aegis
RDM Addle, Magick Barrier
PCT Addle, Tempera Coat, Tempera Grassa

Recast-Zeiten (geplante Datenbank)

Wird benötigt für Konflikt-Erkennung. Beispiele:

  • Reprisal: 60s
  • Feint / Addle: 90s
  • Troubadour / Tactician / Shield Samba: 120s
  • Temperance: 120s
  • Dark Missionary / Heart of Light: 90s

Technische Entscheidungen

  • Persistenz: localStorage — kein Backend nötig, mehrere Pläne möglich
  • IDs: crypto.randomUUID() für Plan- und Mechanik-IDs
  • Merge-Matching: Mechaniken gelten als identisch wenn abilityName gleich und |timestamp_a - timestamp_b| < 5000ms
  • Keine Spielernamen im Planer: Assignments sind Job-basiert ({ ability, job }), damit Pläne übertragbar sind
  • Analyse-Tab Overlay (Prio 7): Job aus tatsächlichem Pull → lookup welche Ability dieser Job im Plan hatte → Soll/Ist-Vergleich