diff --git a/CLAUDE.md b/CLAUDE.md index f7893d1..1df4d29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,10 @@ window.App = { reportCode, fightId, fightStart, fightEnd, phases: [], fights: [] - `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 +`window.plannerTab` (definiert in `planner.js`) stellt Hooks bereit: +- `onTabOpen()` — wird von `tabs.js` aufgerufen wenn der Planer-Tab geöffnet wird +- `importFromAnalysis(aoeEvents, refEvents, options)` — wird vom Analyse-Tab aufgerufen beim Export + ## Dateistruktur ``` index.php — PHP-Logik: Auth-Check, Variablen, require page.php @@ -41,6 +45,7 @@ templates/ 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 + tab-planner.php — Planer-Tab: Plan-Liste, Mechanik-Timeline, Job-Aufstellung (in Entwicklung) 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) @@ -50,10 +55,12 @@ css/ 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 + planner.css — Planer-Tab: Plan-Liste, Mechanik-Cards, Job-Slots, Ability-Modal (in Entwicklung) 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 + planner.js — Planer-Tab: localStorage CRUD, Plan-Rendering, Ability-Zuweisung (in Entwicklung) auth/ start.php — PKCE generieren, Session speichern, Redirect zu FFLogs callback.php — Code gegen Token tauschen, Token in Session speichern @@ -64,6 +71,9 @@ api/ debug-events.php — POST-Endpunkt: Raw Events für Event Explorer (mit Filterung) assets/ icons/mitigation/ — Lokal gespeicherte Ability-Icons (PNG, von XIVAPI) +data/ + recast-times.json — Recast-Zeiten pro Ability (in Entwicklung) + ability-equivalents.json — Funktionale Äquivalente über Jobs hinweg (in Entwicklung) debug/ schema.php — Einmaliges Schema-Explorer Tool (nicht produktiv deployen) ``` @@ -220,7 +230,7 @@ Vollständiges Schema: siehe `debug/schema.php` oder `fflogs-schema.json` ## 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. +Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik eingesetzt? Basierend auf Log-Daten oder manuell aufgebaut. Überlebt Browser-Neustarts via localStorage. Kein Server-State — alles im Browser. ### Datenmodell (Plan) ```json @@ -236,7 +246,9 @@ Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik einges "id": "uuid", "name": "Fourth-Wall Fusion", "timestamp": 83000, + "phase": "Phase 1", "unmitigatedDamage": 280000, + "notes": "", "assignments": [ { "ability": "Reprisal", "job": "PLD" }, { "ability": "Shield Samba", "job": "BRD" } @@ -248,31 +260,42 @@ Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik einges 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. +### Primärer Import-Flow: Export aus dem Analyse-Tab +Der Haupteinstieg ist der Analyse-Tab — der Nutzer hat die Daten bereits geladen und sieht die Timeline. -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) +1. Button **"In Planer exportieren"** erscheint im Analyse-Tab sobald Daten geladen sind (auch für den Ref-Log separat) +2. Dialog mit zwei Entscheidungen: + - **Was importieren?** "Nur Mechaniken" vs. "Mechaniken + erkannte Mitigations als Startpunkt" + - **Wohin?** Neuen Plan anlegen (Name eingeben) vs. bestehenden Plan überschreiben/mergen (Dropdown) +3. Bei Merge: explizite Bestätigung — niemals implizit überschreiben +4. AoE-Events werden zu Mechaniken; Phase-Information aus `phaseTransitions` wird mitübernommen +5. Weiterarbeiten im Planer-Tab -**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. +**Merge-Logik:** Mechaniken gelten als identisch wenn `abilityName` gleich und `|timestamp_a - timestamp_b| < 5000ms`. Nur neue Mechaniken werden hinzugefügt, bestehende Assignments bleiben erhalten. Neue Mechaniken werden timestamp-sortiert eingefügt. -### Geplante Features (Übersicht) +**Warum Merge statt Überschreiben:** Progress-Szenario — erster Import enthält Phase 1, späterer Import fügt Phase 2 hinzu ohne Phase-1-Planung zu verlieren. -| Prio | Feature | Beschreibung | +### Implementierungs-Reihenfolge + +| Schritt | 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 | +| 1 | **Datenfundament** | Plan-Datenmodell + localStorage CRUD (create, read, update, delete, copy) | +| 2 | **Tab-Grundgerüst** | Leere Tab-Hülle wie Analyse-Tab; Plan-Liste; Mechanik-Timeline (read-only) | +| 3 | **Import aus Analyse-Tab** | Export-Button + Dialog (s. oben); `window.plannerTab.importFromAnalysis()` | +| 4 | **Jobaufstellung** | 8 Slots mit Job-Dropdown; bestimmt verfügbare Abilities in Schritt 5 | +| 5 | **Ability-Zuweisung** | Pro Mechanik Abilities per Modal-Picker hinzufügen/entfernen | +| 6 | **Recast-Konflikt** | `data/recast-times.json`; rote Warnung wenn CD zwischen zwei Mechaniken noch läuft | +| 7 | **DR-Simulation** | `simuliert = unmitigated × ∏(1 − dr_i)`; grün/rot ob Spieler laut Simulation überlebt | +| 8 | **Job-Äquivalente** | `data/ability-equivalents.json`; auto-substituieren beim Job-Wechsel | +| 9 | **Analyse-Overlay** | Vergleich geplanter vs. tatsächlich genutzter CDs im Analyse-Tab | + +Schritte 1–3 = nutzbarer MVP. Schritte 4–6 = praktisch einsetzbar. 7–9 = Power-Features. + +### UI-Paradigma +- Visuell dem Analyse-Tab ähneln (Cards, gleiche CSS-Variablen, einheitliches Look & Feel) +- Mechaniken als vertikale Timeline-Cards +- Ability-Picker als **Modal** (kein Inline-Dropdown) +- Nicht für mobile Geräte ausgelegt ### Spell-Verfügbarkeit nach Job Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`): @@ -301,17 +324,31 @@ Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`): | RDM | Addle, Magick Barrier | | PCT | Addle, Tempera Coat, Tempera Grassa | -### Recast-Zeiten (geplante Datenbank) -Wird benötigt für Konflikt-Erkennung. Beispiele: +### Job-Äquivalente (`data/ability-equivalents.json`) +Abilities die funktional gleich sind aber unterschiedliche Namen haben — relevant beim Job-Wechsel im Slot: + +| Gruppe | Abilities | +|---|---| +| 15% Party-Mitigation | Troubadour, Tactician, Shield Samba | +| 10% Ground-Barrier | Sacred Soil, Kerachole | + +Reprisal, Feint und Addle sind identische Ability-Namen über Jobs hinweg — kein Mapping nötig, die gleiche Ability bleibt einfach bestehen. + +**Verhalten beim Job-Wechsel:** Assignment wird auto-substituiert wenn Äquivalent für neuen Job existiert (mit Hinweis "automatisch gemappt"). Kein Äquivalent → Assignment ausgegraut (nicht gelöscht, Nutzer entscheidet). + +### Recast-Zeiten (`data/recast-times.json`) +Wird für Konflikt-Erkennung benötigt (Schritt 6). Vollständige Liste wird beim Implementieren vervollständigt, Beispiele: - Reprisal: 60s - Feint / Addle: 90s +- Dark Missionary / Heart of Light: 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 +- **Persistenz:** `localStorage` unter `ff14-planner-plans` — kein Backend nötig - **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 +- **Keine Spielernamen:** Assignments sind Job-basiert (`{ ability, job }`), damit Pläne übertragbar sind +- **Kein Ability-Stacking:** FFXIV erlaubt keine doppelte Anwendung derselben Ability — jede Ability kommt pro Mechanik maximal einmal vor, doppelte Instanzen desselben Jobs im Static sind daher kein Sonderfall +- **Analyse-Overlay (Schritt 9):** Job aus tatsächlichem Pull → lookup welche Ability dieser Job im Plan hatte → Soll/Ist-Vergleich (kein Spielername-Matching nötig) +- **Shield-Attribution:** Aktuell nicht lösbar — `absorbed` im `damage`-Event ist ein Gesamtwert ohne Aufschlüsselung per Shield. Zu untersuchen: ob `calculatedheal`-Events die Shield-Kapazität beim Auftragen mitliefern. Vorerst zurückgestellt. +- **Plan kopieren:** Duplicate-Funktion für Plan-Varianten ("Week 3 v2") ohne Original zu verlieren diff --git a/css/planner.css b/css/planner.css new file mode 100644 index 0000000..33f27bb --- /dev/null +++ b/css/planner.css @@ -0,0 +1,220 @@ +/* ── Planner Layout ──────────────────────────────────────────────────────────── */ +.planner-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 16px; + align-items: start; +} + +/* ── Plan Sidebar ────────────────────────────────────────────────────────────── */ +.plan-sidebar { + background: var(--bgcard); + border: 1px solid var(--border); + border-radius: var(--rl); + padding: 18px; + position: sticky; + top: 74px; +} + +.plan-sidebar-header { + display: flex; + align-items: center; + margin-bottom: 14px; +} +.plan-sidebar-header .card-title { margin-bottom: 0; flex: 1; } + +.plan-new-form { + background: var(--bg2); + border: 1px solid var(--borderem); + border-radius: var(--r); + padding: 10px; + margin-bottom: 12px; +} +.plan-new-form input { + margin-bottom: 8px; + font-size: 14px; + padding: 6px 10px; +} +.plan-new-actions { + display: flex; + gap: 6px; +} + +.plan-list-empty { + font-size: 13px; + color: var(--t3); + text-align: center; + padding: 24px 0; +} + +/* ── Plan Item ───────────────────────────────────────────────────────────────── */ +.plan-item { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 10px; + border-radius: var(--r); + border: 1px solid transparent; + cursor: pointer; + transition: all 0.12s; + margin-bottom: 4px; +} +.plan-item:hover { background: var(--bg2); border-color: var(--border); } +.plan-item.active { background: var(--bg2); border-color: var(--gold); } + +.plan-item-info { flex: 1; min-width: 0; } + +.plan-item-name { + font-size: 14px; + color: var(--t1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.plan-item.active .plan-item-name { color: var(--gold); } + +.plan-item-meta { + font-size: 12px; + color: var(--t3); + margin-top: 2px; +} + +.plan-item-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.12s; +} +.plan-item:hover .plan-item-actions { opacity: 1; } + +.plan-btn { + background: none; + border: 1px solid var(--borderem); + border-radius: var(--r); + color: var(--t2); + font-size: 13px; + padding: 2px 7px; + cursor: pointer; + transition: all 0.12s; + line-height: 1.6; +} +.plan-btn:hover { background: var(--bg3); color: var(--t1); } +.plan-btn.plan-btn-danger { color: var(--red); border-color: rgba(224,92,92,0.3); } +.plan-btn.plan-btn-danger:hover { background: var(--redbg); } + +/* ── Plan Detail ─────────────────────────────────────────────────────────────── */ +.plan-detail-header { + display: flex; + flex-direction: column; + gap: 5px; +} + +.plan-name-wrap { + display: flex; + align-items: center; + gap: 8px; +} + +.plan-name-text { + font-family: var(--font-d); + font-size: 18px; + color: var(--gold); + letter-spacing: 0.04em; +} + +.plan-name-input { + font-size: 18px !important; + font-family: var(--font-d) !important; + padding: 2px 8px !important; + width: auto !important; + min-width: 200px; + color: var(--gold) !important; +} + +.plan-detail-meta { + font-size: 13px; + color: var(--t3); +} + +/* ── Job Slots Placeholder ───────────────────────────────────────────────────── */ +.job-slots-placeholder { + padding: 20px; + text-align: center; + color: var(--t3); + font-size: 13px; + background: var(--bg2); + border: 1px dashed var(--border); + border-radius: var(--r); +} + +/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */ +.mechanic-card { + display: grid; + grid-template-columns: 52px 1fr; + gap: 14px; + padding: 12px 0; + border-bottom: 1px solid var(--border); + align-items: start; +} +.mechanic-card:last-child { border-bottom: none; } + +.mechanic-time { + font-family: var(--font-d); + font-size: 14px; + color: var(--gold); + letter-spacing: 0.03em; + padding-top: 3px; +} + +.mechanic-body { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mechanic-phase { + font-size: 11px; + color: var(--blue); + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.mechanic-name { + font-size: 15px; + color: var(--t1); + font-weight: 500; +} + +.mechanic-dmg { + font-size: 13px; + color: var(--t2); +} + +.mechanic-assignments { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 2px; +} + +.mechanic-no-assign { + font-size: 12px; + color: var(--t3); +} + +.badge-assign { + background: var(--bg2); + border: 1px solid var(--borderem); + border-radius: var(--r); + padding: 2px 8px; + font-size: 12px; + color: var(--t2); +} + +.mechanic-notes { + font-size: 12px; + color: var(--t3); + font-style: italic; + margin-top: 2px; +} diff --git a/js/planner.js b/js/planner.js new file mode 100644 index 0000000..c7549e5 --- /dev/null +++ b/js/planner.js @@ -0,0 +1,333 @@ +// ── Storage ─────────────────────────────────────────────────────────────────── + +const PLANNER_KEY = 'ff14-planner-plans'; + +function loadPlans() { + try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); } + catch { return []; } +} + +function savePlans(plans) { + localStorage.setItem(PLANNER_KEY, JSON.stringify(plans)); +} + +// ── CRUD ────────────────────────────────────────────────────────────────────── + +function createPlan(name) { + const plan = { + id: crypto.randomUUID(), + name: name.trim() || 'Unbenannter Plan', + createdAt: Date.now(), + updatedAt: Date.now(), + source: null, + jobComposition: Array(8).fill(''), + mechanics: [] + }; + const all = loadPlans(); + all.push(plan); + savePlans(all); + return plan; +} + +function getPlan(id) { + return loadPlans().find(p => p.id === id) ?? null; +} + +function updatePlan(id, changes) { + const all = loadPlans(); + const idx = all.findIndex(p => p.id === id); + if (idx === -1) return null; + all[idx] = { ...all[idx], ...changes, updatedAt: Date.now() }; + savePlans(all); + return all[idx]; +} + +function deletePlan(id) { + savePlans(loadPlans().filter(p => p.id !== id)); +} + +function copyPlan(id) { + const all = loadPlans(); + const orig = all.find(p => p.id === id); + if (!orig) return null; + const copy = { + ...JSON.parse(JSON.stringify(orig)), + id: crypto.randomUUID(), + name: orig.name + ' (Kopie)', + createdAt: Date.now(), + updatedAt: Date.now() + }; + all.push(copy); + savePlans(all); + return copy; +} + +// ── State ───────────────────────────────────────────────────────────────────── + +let activePlanId = null; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function escHtml(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function fmtDate(ts) { + return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function fmtTimestamp(ms) { + const s = Math.floor(ms / 1000); + return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; +} + +function fmtNumber(n) { + return Number(n).toLocaleString('de-DE'); +} + +// ── Rendering: Plan List ────────────────────────────────────────────────────── + +function renderPlanList() { + const el = document.getElementById('plan-list'); + if (!el) return; + + const plans = loadPlans(); + + if (plans.length === 0) { + el.innerHTML = '
+ Importiere einen Log aus dem Analyse-Tab +
+Erstelle einen neuen Plan oder wähle einen bestehenden aus
+