forked from xziino/ff14-mitigator
Compare commits
24 Commits
19bd79c056
...
4801148a8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4801148a8c | ||
|
|
9a2c1490de | ||
|
|
15754fefda | ||
|
|
86d2a106bb | ||
|
|
1c0918a4a3 | ||
|
|
d585f3be5a | ||
|
|
c3f18b38f5 | ||
|
|
c67b08737e | ||
|
|
84064669d3 | ||
|
|
be9d050036 | ||
|
|
5010da4ddb | ||
|
|
14674d2842 | ||
|
|
7c6f443a53 | ||
|
|
89b4849ed4 | ||
| cc36aa27c4 | |||
|
|
969484a1dc | ||
|
|
8f29619ef5 | ||
|
|
e2eed52d07 | ||
|
|
d73dd340c2 | ||
|
|
8f832e1a0a | ||
|
|
ed4b72654b | ||
|
|
4107779e2a | ||
|
|
ea00268227 | ||
|
|
9ff8139c81 |
93
CLAUDE.md
93
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)
|
- `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
|
- `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
|
## Dateistruktur
|
||||||
```
|
```
|
||||||
index.php — PHP-Logik: Auth-Check, Variablen, require page.php
|
index.php — PHP-Logik: Auth-Check, Variablen, require page.php
|
||||||
@ -41,6 +45,7 @@ templates/
|
|||||||
topbar.php — Topbar mit Logo + Tab-Navigation + Token-Ablaufzeit
|
topbar.php — Topbar mit Logo + Tab-Navigation + Token-Ablaufzeit
|
||||||
tab-report.php — Report-Tab: includes report-form, fight-select, event-explorer, output-card
|
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-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
|
report-form.php — Report-Code-Eingabe Card
|
||||||
fight-select.php — Fight-Auswahl Dropdown Card
|
fight-select.php — Fight-Auswahl Dropdown Card
|
||||||
event-explorer.php — Event Explorer Card (Ability/DataType/EventType/Spieler-Filter)
|
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
|
layout.css — App-Shell, Topbar, Tabs, Login-Overlay, Form-Helpers
|
||||||
components.css — Cards, Inputs, Buttons, Badges, Terminal
|
components.css — Cards, Inputs, Buttons, Badges, Terminal
|
||||||
analysis.css — Spieler-Grid, AoE-Timeline, Mitigation-Icons, HP-Bar, Ref-Row
|
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/
|
js/
|
||||||
app.js — Formular, Fight-Dropdown, Fetch, window.App State, Event Explorer
|
app.js — Formular, Fight-Dropdown, Fetch, window.App State, Event Explorer
|
||||||
tabs.js — Tab-Switching, ruft window.analysisTab.onTabOpen() auf
|
tabs.js — Tab-Switching, ruft window.analysisTab.onTabOpen() auf
|
||||||
analysis.js — Analyse-Tab: Daten laden, Spieler rendern, Timeline rendern, Pull-Vergleich
|
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/
|
auth/
|
||||||
start.php — PKCE generieren, Session speichern, Redirect zu FFLogs
|
start.php — PKCE generieren, Session speichern, Redirect zu FFLogs
|
||||||
callback.php — Code gegen Token tauschen, Token in Session speichern
|
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)
|
debug-events.php — POST-Endpunkt: Raw Events für Event Explorer (mit Filterung)
|
||||||
assets/
|
assets/
|
||||||
icons/mitigation/ — Lokal gespeicherte Ability-Icons (PNG, von XIVAPI)
|
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/
|
debug/
|
||||||
schema.php — Einmaliges Schema-Explorer Tool (nicht produktiv deployen)
|
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
|
## Planer-Tab — Konzept & Roadmap
|
||||||
|
|
||||||
### Ziel
|
### 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)
|
### Datenmodell (Plan)
|
||||||
```json
|
```json
|
||||||
@ -236,7 +246,9 @@ Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik einges
|
|||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
"name": "Fourth-Wall Fusion",
|
"name": "Fourth-Wall Fusion",
|
||||||
"timestamp": 83000,
|
"timestamp": 83000,
|
||||||
|
"phase": "Phase 1",
|
||||||
"unmitigatedDamage": 280000,
|
"unmitigatedDamage": 280000,
|
||||||
|
"notes": "",
|
||||||
"assignments": [
|
"assignments": [
|
||||||
{ "ability": "Reprisal", "job": "PLD" },
|
{ "ability": "Reprisal", "job": "PLD" },
|
||||||
{ "ability": "Shield Samba", "job": "BRD" }
|
{ "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.
|
Mehrere Pläne gespeichert in `localStorage` unter `ff14-planner-plans` als Array.
|
||||||
|
|
||||||
### Import-Flow (erster Meilenstein)
|
### Primärer Import-Flow: Export aus dem Analyse-Tab
|
||||||
**Ziel:** Einen bestehenden Log als saubere Mechanik-Vorlage laden — ohne vorhandene Mechaniken zu überschreiben.
|
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)
|
1. Button **"In Planer exportieren"** erscheint im Analyse-Tab sobald Daten geladen sind (auch für den Ref-Log separat)
|
||||||
2. `api/analysis.php` wird aufgerufen → liefert `aoe_events` mit Name, Timestamp, `unmitigatedAmount`
|
2. Dialog mit zwei Entscheidungen:
|
||||||
3. Jedes AoE-Event wird als Mechanik-Kandidat angezeigt (Name + Timestamp + Rohschaden)
|
- **Was importieren?** "Nur Mechaniken" vs. "Mechaniken + erkannte Mitigations als Startpunkt"
|
||||||
4. Benutzer kann einzelne Events auswählen oder alle übernehmen
|
- **Wohin?** Neuen Plan anlegen (Name eingeben) vs. bestehenden Plan überschreiben/mergen (Dropdown)
|
||||||
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.
|
3. Bei Merge: explizite Bestätigung — niemals implizit überschreiben
|
||||||
6. Neue Mechaniken werden an der richtigen Timestamp-Position eingefügt (Timeline bleibt sortiert)
|
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 |
|
| 1 | **Datenfundament** | Plan-Datenmodell + localStorage CRUD (create, read, update, delete, copy) |
|
||||||
| 2 | **Jobaufstellung** | 8 Slots (2 Tank, 2 Healer, 4 DPS), Auswahl bestimmt verfügbare Spells |
|
| 2 | **Tab-Grundgerüst** | Leere Tab-Hülle wie Analyse-Tab; Plan-Liste; Mechanik-Timeline (read-only) |
|
||||||
| 3 | **Cooldown-Zuweisung** | Pro Mechanik Abilities zuweisen/entfernen per Klick |
|
| 3 | **Import aus Analyse-Tab** | Export-Button + Dialog (s. oben); `window.plannerTab.importFromAnalysis()` |
|
||||||
| 4 | **DR-Simulation** | `simuliert = unmitigated × ∏(1 − dr_i)` live berechnet beim Toggle |
|
| 4 | **Jobaufstellung** | 8 Slots mit Job-Dropdown; bestimmt verfügbare Abilities in Schritt 5 |
|
||||||
| 5 | **Recast-Tracking** | Recast-Datenbank pro Ability; Konflikt-Warnung wenn CD noch läuft |
|
| 5 | **Ability-Zuweisung** | Pro Mechanik Abilities per Modal-Picker hinzufügen/entfernen |
|
||||||
| 6 | **Coverage-Ansicht** | Gantt-Chart: Mechaniken auf X-Achse, Buff-Dauer als Balken |
|
| 6 | **Recast-Konflikt** | `data/recast-times.json`; rote Warnung wenn CD zwischen zwei Mechaniken noch läuft |
|
||||||
| 7 | **Analyse-Overlay** | Planer-Tab: Vergleich geplanter vs. tatsächlich genutzter CDs (Job-basiertes Matching, nicht Spielername) |
|
| 7 | **DR-Simulation** | `simuliert = unmitigated × ∏(1 − dr_i)`; grün/rot ob Spieler laut Simulation überlebt |
|
||||||
| 8 | **Shield-Schätzung** | Empirisch aus Log-Durchschnitt (`absorbed`-Werte), nicht exakt |
|
| 8 | **Job-Äquivalente** | `data/ability-equivalents.json`; auto-substituieren beim Job-Wechsel |
|
||||||
| 9 | **JSON-Export/Import** | Plan als Datei teilen mit Raidkollegen |
|
| 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
|
### Spell-Verfügbarkeit nach Job
|
||||||
Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`):
|
Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`):
|
||||||
@ -301,17 +324,31 @@ Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`):
|
|||||||
| RDM | Addle, Magick Barrier |
|
| RDM | Addle, Magick Barrier |
|
||||||
| PCT | Addle, Tempera Coat, Tempera Grassa |
|
| PCT | Addle, Tempera Coat, Tempera Grassa |
|
||||||
|
|
||||||
### Recast-Zeiten (geplante Datenbank)
|
### Job-Äquivalente (`data/ability-equivalents.json`)
|
||||||
Wird benötigt für Konflikt-Erkennung. Beispiele:
|
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
|
- Reprisal: 60s
|
||||||
- Feint / Addle: 90s
|
- Feint / Addle: 90s
|
||||||
|
- Dark Missionary / Heart of Light: 90s
|
||||||
- Troubadour / Tactician / Shield Samba: 120s
|
- Troubadour / Tactician / Shield Samba: 120s
|
||||||
- Temperance: 120s
|
- Temperance: 120s
|
||||||
- Dark Missionary / Heart of Light: 90s
|
|
||||||
|
|
||||||
### Technische Entscheidungen
|
### 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
|
- **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:** Assignments sind Job-basiert (`{ ability, job }`), damit Pläne übertragbar sind
|
||||||
- **Keine Spielernamen im Planer:** 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-Tab Overlay (Prio 7):** Job aus tatsächlichem Pull → lookup welche Ability dieser Job im Plan hatte → Soll/Ist-Vergleich
|
- **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
|
||||||
|
|||||||
@ -212,11 +212,12 @@ foreach (MITIGATION_ABILITIES as $name => $meta) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// statusId set for shield abilities — used to filter the buff timeline query
|
// statusId set for tracked mitigations — used to resolve localized buff names
|
||||||
$shieldStatusIds = [];
|
// from Buffs events and to build the shield fallback timeline.
|
||||||
|
$trackedStatusIds = [];
|
||||||
foreach (MITIGATION_ABILITIES as $meta) {
|
foreach (MITIGATION_ABILITIES as $meta) {
|
||||||
if ($meta['buffType'] === 'shield' && isset($meta['statusId'])) {
|
if (isset($meta['statusId'])) {
|
||||||
$shieldStatusIds[$meta['statusId']] = true;
|
$trackedStatusIds[$meta['statusId']] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,8 +277,9 @@ for ($page = 0; $page < 10; $page++) {
|
|||||||
// Builds applybuff/removebuff intervals per target so we can detect shields
|
// Builds applybuff/removebuff intervals per target so we can detect shields
|
||||||
// that were consumed by a hit (absent from the damage event's buffs snapshot).
|
// that were consumed by a hit (absent from the damage event's buffs snapshot).
|
||||||
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
|
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
|
||||||
|
$statusNames = []; // statusId → localized display name from Buffs events
|
||||||
|
|
||||||
if (!empty($shieldStatusIds)) {
|
if (!empty($trackedStatusIds)) {
|
||||||
$nextPage = $startTime;
|
$nextPage = $startTime;
|
||||||
for ($page = 0; $page < 10; $page++) {
|
for ($page = 0; $page < 10; $page++) {
|
||||||
$bfResult = fflogs_gql(<<<GQL
|
$bfResult = fflogs_gql(<<<GQL
|
||||||
@ -304,11 +306,19 @@ if (!empty($shieldStatusIds)) {
|
|||||||
$bfEv = $bfResult['data']['reportData']['report']['events'] ?? [];
|
$bfEv = $bfResult['data']['reportData']['report']['events'] ?? [];
|
||||||
foreach ($bfEv['data'] ?? [] as $ev) {
|
foreach ($bfEv['data'] ?? [] as $ev) {
|
||||||
$abId = (int)($ev['abilityGameID'] ?? 0);
|
$abId = (int)($ev['abilityGameID'] ?? 0);
|
||||||
if (!isset($shieldStatusIds[$abId])) continue;
|
if (!isset($trackedStatusIds[$abId])) continue;
|
||||||
|
|
||||||
|
$evName = $ev['ability']['name'] ?? null;
|
||||||
|
if (is_string($evName) && $evName !== '') {
|
||||||
|
$statusNames[$abId] = $evName;
|
||||||
|
}
|
||||||
|
|
||||||
$tgtId = (int)($ev['targetID'] ?? 0);
|
$tgtId = (int)($ev['targetID'] ?? 0);
|
||||||
$ts = (float)($ev['timestamp'] ?? 0);
|
$ts = (float)($ev['timestamp'] ?? 0);
|
||||||
$type = $ev['type'] ?? '';
|
$type = $ev['type'] ?? '';
|
||||||
|
$meta = $mitigIdMap[$abId] ?? null;
|
||||||
|
|
||||||
|
if (($meta['buffType'] ?? null) !== 'shield') continue;
|
||||||
|
|
||||||
if ($type === 'applybuff') {
|
if ($type === 'applybuff') {
|
||||||
$shieldTimeline[$tgtId][$abId][] = ['apply' => $ts, 'remove' => null];
|
$shieldTimeline[$tgtId][$abId][] = ['apply' => $ts, 'remove' => null];
|
||||||
@ -329,6 +339,20 @@ if (!empty($shieldStatusIds)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($statusNames as $statusId => $displayName) {
|
||||||
|
if (isset($mitigIdMap[$statusId])) {
|
||||||
|
$mitigIdMap[$statusId]['name'] = $displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mitigationNames = [];
|
||||||
|
foreach ($mitigIdMap as $meta) {
|
||||||
|
$key = $meta['key'] ?? null;
|
||||||
|
if ($key) {
|
||||||
|
$mitigationNames[$key] = $meta['name'] ?? $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 3. AoE detection — proximity clustering ────────────────────────────────
|
// ── 3. AoE detection — proximity clustering ────────────────────────────────
|
||||||
// Group events by abilityId, then cluster by time proximity (≤ 1000ms from
|
// Group events by abilityId, then cluster by time proximity (≤ 1000ms from
|
||||||
// the first event in the cluster) to avoid fixed-window boundary splits.
|
// the first event in the cluster) to avoid fixed-window boundary splits.
|
||||||
@ -477,4 +501,5 @@ echo json_encode([
|
|||||||
'players' => array_values($players),
|
'players' => array_values($players),
|
||||||
'aoe_events' => $aoeEvents,
|
'aoe_events' => $aoeEvents,
|
||||||
'fight_start' => (int)$startTime,
|
'fight_start' => (int)$startTime,
|
||||||
|
'mitigation_names' => $mitigationNames,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -75,8 +75,9 @@ function localized_graphql_uri(string $language): string {
|
|||||||
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
|
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
$acceptLanguage = $language === 'jp' ? 'ja' : $language;
|
// Fight names must be stable regardless of language — always use the English endpoint.
|
||||||
$ch = curl_init(localized_graphql_uri($language));
|
// Localization only matters for ability/player names in analysis.php.
|
||||||
|
$ch = curl_init(GRAPHQL_URI);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_POST => true,
|
CURLOPT_POST => true,
|
||||||
CURLOPT_POSTFIELDS => $payload,
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
@ -84,7 +85,6 @@ curl_setopt_array($ch, [
|
|||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'Authorization: Bearer ' . $_SESSION['access_token'],
|
'Authorization: Bearer ' . $_SESSION['access_token'],
|
||||||
'Accept-Language: ' . $acceptLanguage,
|
|
||||||
],
|
],
|
||||||
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ $state = bin2hex(random_bytes(16));
|
|||||||
|
|
||||||
$_SESSION['pkce_verifier'] = $verifier;
|
$_SESSION['pkce_verifier'] = $verifier;
|
||||||
$_SESSION['oauth_state'] = $state;
|
$_SESSION['oauth_state'] = $state;
|
||||||
$_SESSION['oauth_return'] = safe_return_path($_GET['return'] ?? ($_SERVER['HTTP_REFERER'] ?? null));
|
$_SESSION['oauth_return'] = null;
|
||||||
|
|
||||||
$params = http_build_query([
|
$params = http_build_query([
|
||||||
'response_type' => 'code',
|
'response_type' => 'code',
|
||||||
|
|||||||
599
css/planner.css
Normal file
599
css/planner.css
Normal file
@ -0,0 +1,599 @@
|
|||||||
|
/* ── 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 ────────────────────────────────────────────────────────────────── */
|
||||||
|
.job-slots-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-slot {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-slot select {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
border-left: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-slot--tank select { border-left-color: var(--blue); }
|
||||||
|
.job-slot--healer select { border-left-color: var(--green); }
|
||||||
|
.job-slot--dps select { border-left-color: rgba(200,168,75,.5); }
|
||||||
|
|
||||||
|
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
|
||||||
|
.mechanic-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 52px 1fr auto;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 12px 6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
align-items: start;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
border-radius: var(--r);
|
||||||
|
margin: 0 -6px;
|
||||||
|
}
|
||||||
|
.mechanic-card:last-child { border-bottom: none; }
|
||||||
|
.mechanic-card:hover { background: var(--bg2); }
|
||||||
|
|
||||||
|
.mechanic-delete-btn {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.mechanic-card:hover .mechanic-delete-btn { opacity: 1; }
|
||||||
|
|
||||||
|
.mechanic-edit-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t3);
|
||||||
|
margin-top: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
.mechanic-card:hover .mechanic-edit-hint { opacity: 1; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mechanic-no-assign {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-assign {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--borderem);
|
||||||
|
border-radius: var(--r);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--t2);
|
||||||
|
}
|
||||||
|
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||||
|
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||||
|
.badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||||
|
|
||||||
|
.badge-assign--missing-job {
|
||||||
|
border-style: dashed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
.badge-assign:hover .badge-remove { opacity: 0.6; }
|
||||||
|
.badge-remove:hover { opacity: 1 !important; }
|
||||||
|
|
||||||
|
.mechanic-notes {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t3);
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Import Modal ────────────────────────────────────────────────────────────── */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-box {
|
||||||
|
background: var(--bgcard);
|
||||||
|
border: 1px solid var(--borderem);
|
||||||
|
border-radius: var(--rl);
|
||||||
|
padding: 28px 32px;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-family: var(--font-d);
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--gold);
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-radio-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--t1);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
/* Override global input styles that break radio button layout */
|
||||||
|
.modal-radio-label input[type="radio"] {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subsection {
|
||||||
|
margin-left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.modal-subsection input,
|
||||||
|
.modal-subsection select {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ability Assignment Modal ────────────────────────────────────────────────── */
|
||||||
|
.ability-modal-box {
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-job-group {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-job-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--borderem);
|
||||||
|
border-radius: var(--r);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
.ability-chip:hover { background: var(--bg3); color: var(--t1); }
|
||||||
|
|
||||||
|
.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); }
|
||||||
|
.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); }
|
||||||
|
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
|
||||||
|
|
||||||
|
.ability-chip--other-job { opacity: 0.45; }
|
||||||
|
|
||||||
|
/* ── Equivalents hint ────────────────────────────────────────────────────────── */
|
||||||
|
.badge-with-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-equiv-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-no-equiv-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--red);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-job-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t3);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: default;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info Panel ──────────────────────────────────────────────────────────────── */
|
||||||
|
.planner-info-panel {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section { margin-bottom: 12px; }
|
||||||
|
.info-section:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.info-section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-legend-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-legend-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.info-legend-dot--buff { background: rgba(200,168,75,.8); }
|
||||||
|
.info-legend-dot--debuff { background: var(--red); }
|
||||||
|
.info-legend-dot--shield { background: var(--blue); }
|
||||||
|
|
||||||
|
.info-legend-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section--warnings { margin-top: 12px; }
|
||||||
|
|
||||||
|
.info-warning {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
.info-warning--job { color: var(--t2); }
|
||||||
|
.info-warning--missing { color: var(--red); }
|
||||||
|
|
||||||
|
/* ── Folder Sidebar ──────────────────────────────────────────────────────────── */
|
||||||
|
.folder-section { margin-bottom: 2px; }
|
||||||
|
|
||||||
|
.folder-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.folder-row:hover { background: var(--bg2); }
|
||||||
|
|
||||||
|
.folder-chevron {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--t3);
|
||||||
|
transition: transform 0.15s;
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.folder-chevron.open { transform: rotate(90deg); }
|
||||||
|
|
||||||
|
.folder-name-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--t1);
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-name-input {
|
||||||
|
font-size: 13px !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
flex: 1;
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t3);
|
||||||
|
background: var(--bg3);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
.folder-row:hover .folder-actions { opacity: 1; }
|
||||||
|
|
||||||
|
.folder-plans { padding-left: 10px; }
|
||||||
|
.folder-plans.collapsed { display: none; }
|
||||||
|
|
||||||
|
.folder-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t3);
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Folder Picker Dropdown ──────────────────────────────────────────────────── */
|
||||||
|
.folder-picker {
|
||||||
|
background: var(--bgcard);
|
||||||
|
border: 1px solid var(--borderem);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
||||||
|
min-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker-option {
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--t2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.folder-picker-option:hover { background: var(--bg2); color: var(--t1); }
|
||||||
|
.folder-picker-option.active { color: var(--gold); }
|
||||||
@ -103,6 +103,7 @@
|
|||||||
let currentPlayers = [];
|
let currentPlayers = [];
|
||||||
let extFights = [];
|
let extFights = [];
|
||||||
let extReportCode = '';
|
let extReportCode = '';
|
||||||
|
let mitigationNames = {};
|
||||||
|
|
||||||
// ── Player grid ──────────────────────────────────────────────────────────
|
// ── Player grid ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -683,10 +684,14 @@
|
|||||||
populateRefFightSelect();
|
populateRefFightSelect();
|
||||||
setupPhases(window.App?.phases ?? []);
|
setupPhases(window.App?.phases ?? []);
|
||||||
renderPlayers(json.players ?? []);
|
renderPlayers(json.players ?? []);
|
||||||
|
mitigationNames = json.mitigation_names ?? {};
|
||||||
renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart);
|
renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart);
|
||||||
|
|
||||||
document.getElementById('analysis-loading').style.display = 'none';
|
document.getElementById('analysis-loading').style.display = 'none';
|
||||||
document.getElementById('analysis-content').style.display = 'block';
|
document.getElementById('analysis-content').style.display = 'block';
|
||||||
|
|
||||||
|
const exportBtn = document.getElementById('export-to-planner-btn');
|
||||||
|
if (exportBtn) exportBtn.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.analysisTab = {
|
window.analysisTab = {
|
||||||
@ -705,6 +710,20 @@
|
|||||||
refFightSelect.dispatchEvent(new Event('change'));
|
refFightSelect.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
exportForPlanner() {
|
||||||
|
const fight = (window.App?.fights ?? []).find(f => f.id === window.App?.fightId);
|
||||||
|
return {
|
||||||
|
aoeEvents: lastEvents,
|
||||||
|
fightStart: lastFightStart,
|
||||||
|
phases: window.App?.phases ?? [],
|
||||||
|
players: currentPlayers,
|
||||||
|
fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`,
|
||||||
|
reportCode: window.App?.reportCode ?? '',
|
||||||
|
fightId: window.App?.fightId ?? 0,
|
||||||
|
fightEnd: window.App?.fightEnd ?? 0,
|
||||||
|
mitigationNames,
|
||||||
|
};
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
lastFightId = null;
|
lastFightId = null;
|
||||||
refEvents = [];
|
refEvents = [];
|
||||||
@ -712,6 +731,7 @@
|
|||||||
refPlayers = [];
|
refPlayers = [];
|
||||||
extFights = [];
|
extFights = [];
|
||||||
extReportCode = '';
|
extReportCode = '';
|
||||||
|
mitigationNames = {};
|
||||||
document.getElementById('ref-player-section').style.display = 'none';
|
document.getElementById('ref-player-section').style.display = 'none';
|
||||||
refFightSelect.value = '';
|
refFightSelect.value = '';
|
||||||
refFightSelect.style.display = 'none';
|
refFightSelect.style.display = 'none';
|
||||||
@ -720,6 +740,12 @@
|
|||||||
refFflogsLink.style.display = 'none';
|
refFflogsLink.style.display = 'none';
|
||||||
refFflogsLink.href = '#';
|
refFflogsLink.href = '#';
|
||||||
refExtPanel.style.display = 'none';
|
refExtPanel.style.display = 'none';
|
||||||
|
const exportBtn = document.getElementById('export-to-planner-btn');
|
||||||
|
if (exportBtn) exportBtn.style.display = 'none';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('export-to-planner-btn')?.addEventListener('click', () => {
|
||||||
|
window.plannerTab?.showImportModal(window.analysisTab.exportForPlanner());
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -103,6 +103,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
window.App.language = normalizeLanguage(languageSelect.value);
|
window.App.language = normalizeLanguage(languageSelect.value);
|
||||||
localStorage.setItem('ff14-mitigator-language', window.App.language);
|
localStorage.setItem('ff14-mitigator-language', window.App.language);
|
||||||
setUrlState({ language: window.App.language });
|
setUrlState({ language: window.App.language });
|
||||||
|
window.dispatchEvent(new CustomEvent('ff14-language-change', { detail: { language: window.App.language } }));
|
||||||
if (window.App.reportCode) {
|
if (window.App.reportCode) {
|
||||||
loadReport(window.App.reportCode, window.App.fightId);
|
loadReport(window.App.reportCode, window.App.fightId);
|
||||||
}
|
}
|
||||||
@ -136,6 +137,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.querySelector('.tabs .tab[data-tab="analysis"]')?.click();
|
document.querySelector('.tabs .tab[data-tab="analysis"]')?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldAutoOpenAnalysis() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const requestedTab = params.get('tab') || localStorage.getItem('ff14-mitigator-active-tab');
|
||||||
|
return requestedTab !== 'planner';
|
||||||
|
}
|
||||||
|
|
||||||
function selectFight(id, updateUrl = true) {
|
function selectFight(id, updateUrl = true) {
|
||||||
const fight = allFights.find(f => f.id === id);
|
const fight = allFights.find(f => f.id === id);
|
||||||
if (!fight) return false;
|
if (!fight) return false;
|
||||||
@ -340,7 +347,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (initialUrlState.reportCode) {
|
if (initialUrlState.reportCode) {
|
||||||
form.elements['report_code'].value = initialUrlState.reportCode;
|
form.elements['report_code'].value = initialUrlState.reportCode;
|
||||||
loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => {
|
loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => {
|
||||||
if (initialUrlState.fightId) {
|
if (initialUrlState.fightId && shouldAutoOpenAnalysis()) {
|
||||||
openAnalysisTab();
|
openAnalysisTab();
|
||||||
}
|
}
|
||||||
if (initialUrlState.compareFightId) {
|
if (initialUrlState.compareFightId) {
|
||||||
|
|||||||
1378
js/planner.js
Normal file
1378
js/planner.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,10 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const tabs = document.querySelectorAll('.tabs .tab');
|
const tabs = document.querySelectorAll('.tabs .tab');
|
||||||
const contents = document.querySelectorAll('.tab-content');
|
const contents = document.querySelectorAll('.tab-content');
|
||||||
|
const validTabs = new Set([...tabs].map(btn => btn.dataset.tab));
|
||||||
|
|
||||||
function showTab(name) {
|
function showTab(name) {
|
||||||
|
if (!validTabs.has(name)) name = 'report';
|
||||||
contents.forEach(el => el.style.display = 'none');
|
contents.forEach(el => el.style.display = 'none');
|
||||||
tabs.forEach(btn => btn.classList.remove('active'));
|
tabs.forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
@ -13,7 +15,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (btn) btn.classList.add('active');
|
if (btn) btn.classList.add('active');
|
||||||
|
|
||||||
if (name === 'analysis') window.analysisTab?.onTabOpen?.();
|
if (name === 'analysis') window.analysisTab?.onTabOpen?.();
|
||||||
|
if (name === 'planner') window.plannerTab?.onTabOpen?.();
|
||||||
|
|
||||||
|
localStorage.setItem('ff14-mitigator-active-tab', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
|
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
|
||||||
|
window.showTab = showTab;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
showTab(params.get('tab') || localStorage.getItem('ff14-mitigator-active-tab') || 'report');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>" class="btn btn-gold btn-login">
|
<a href="auth/start.php" class="btn btn-gold btn-login">
|
||||||
<?= $tokenExpired ? 'Reconnect to FFLogs' : 'Connect to FFLogs' ?>
|
<?= $tokenExpired ? 'Reconnect to FFLogs' : 'Connect to FFLogs' ?>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
<link rel="stylesheet" href="css/layout.css">
|
<link rel="stylesheet" href="css/layout.css">
|
||||||
<link rel="stylesheet" href="css/components.css">
|
<link rel="stylesheet" href="css/components.css">
|
||||||
<link rel="stylesheet" href="css/analysis.css">
|
<link rel="stylesheet" href="css/analysis.css">
|
||||||
|
<link rel="stylesheet" href="css/planner.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@ -28,11 +29,72 @@
|
|||||||
<div id="tab-analysis" class="tab-content" style="display:none">
|
<div id="tab-analysis" class="tab-content" style="display:none">
|
||||||
<?php require __DIR__ . '/tab-analysis.php'; ?>
|
<?php require __DIR__ . '/tab-analysis.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-planner" class="tab-content" style="display:none">
|
||||||
|
<?php require __DIR__ . '/tab-planner.php'; ?>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Planner Import Modal -->
|
||||||
|
<div id="planner-import-modal" class="modal-overlay" style="display:none">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-title">In Planer exportieren</div>
|
||||||
|
|
||||||
|
<div class="modal-section">
|
||||||
|
<div class="modal-label">Was importieren?</div>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-what" value="with-mitigations" checked>
|
||||||
|
<span>Mechaniken + Mitigation</span>
|
||||||
|
</label>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-what" value="mechanics">
|
||||||
|
<span>Nur Mechaniken</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-section">
|
||||||
|
<div class="modal-label">Wohin?</div>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-where" value="new" checked>
|
||||||
|
<span>Neuer Plan</span>
|
||||||
|
</label>
|
||||||
|
<div id="import-new-section" class="modal-subsection">
|
||||||
|
<input type="text" id="import-plan-name" placeholder="Plan-Name…">
|
||||||
|
</div>
|
||||||
|
<label class="modal-radio-label" id="import-merge-label">
|
||||||
|
<input type="radio" name="import-where" value="merge">
|
||||||
|
<span>In bestehenden Plan mergen</span>
|
||||||
|
</label>
|
||||||
|
<div id="import-merge-section" class="modal-subsection" style="display:none">
|
||||||
|
<select id="import-plan-select">
|
||||||
|
<option value="">— Plan auswählen —</option>
|
||||||
|
</select>
|
||||||
|
<div class="modal-hint">Neue Mechaniken werden hinzugefügt, bestehende Assignments bleiben erhalten.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="import-confirm-btn" class="btn btn-gold">Importieren</button>
|
||||||
|
<button id="import-cancel-btn" class="btn">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ability Assignment Modal -->
|
||||||
|
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
|
||||||
|
<div class="modal-box ability-modal-box">
|
||||||
|
<div class="modal-title" id="ability-modal-title">Mitigations</div>
|
||||||
|
<div id="ability-modal-content"></div>
|
||||||
|
<div class="modal-actions" style="margin-top:16px">
|
||||||
|
<button id="ability-modal-close" class="btn">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
<script src="js/tabs.js"></script>
|
<script src="js/tabs.js"></script>
|
||||||
<script src="js/analysis.js"></script>
|
<script src="js/analysis.js"></script>
|
||||||
|
<script src="js/planner.js"></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-gold" type="submit" style="align-self:flex-end">Fetch</button>
|
<button class="btn btn-gold" type="submit" style="align-self:flex-end">Fetch</button>
|
||||||
<a class="btn" href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>" style="align-self:flex-end;text-decoration:none">Reconnect</a>
|
<a class="btn" href="auth/start.php" style="align-self:flex-end;text-decoration:none">Reconnect</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -40,6 +40,7 @@
|
|||||||
<div class="card-title">AoE Timeline</div>
|
<div class="card-title">AoE Timeline</div>
|
||||||
<select id="phase-select" class="filter-input" style="display:none"></select>
|
<select id="phase-select" class="filter-input" style="display:none"></select>
|
||||||
<input type="text" id="player-filter" class="filter-input" placeholder="Spieler filtern…">
|
<input type="text" id="player-filter" class="filter-input" placeholder="Spieler filtern…">
|
||||||
|
<button id="export-to-planner-btn" class="btn btn-sm btn-gold" style="display:none">📋 Exportieren</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="aoe-timeline"></div>
|
<div id="aoe-timeline"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
42
templates/tab-planner.php
Normal file
42
templates/tab-planner.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<div class="planner-layout">
|
||||||
|
|
||||||
|
<!-- Left: Plan list sidebar -->
|
||||||
|
<div class="plan-sidebar">
|
||||||
|
<div class="plan-sidebar-header">
|
||||||
|
<div class="card-title">Pläne</div>
|
||||||
|
<button id="planner-new-folder-btn" class="btn btn-sm" title="Neuer Ordner">+ Ordner</button>
|
||||||
|
<button id="planner-new-btn" class="btn btn-sm btn-gold">+ Neu</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="planner-new-folder-form" class="plan-new-form" style="display:none">
|
||||||
|
<input type="text" id="planner-new-folder-name" placeholder="Ordner-Name…">
|
||||||
|
<div class="plan-new-actions">
|
||||||
|
<button id="planner-new-folder-save" class="btn btn-sm btn-gold">Erstellen</button>
|
||||||
|
<button id="planner-new-folder-cancel" class="btn btn-sm">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="planner-new-form" class="plan-new-form" style="display:none">
|
||||||
|
<input type="text" id="planner-new-name" placeholder="Plan-Name…">
|
||||||
|
<div class="plan-new-actions">
|
||||||
|
<button id="planner-new-save" class="btn btn-sm btn-gold">Erstellen</button>
|
||||||
|
<button id="planner-new-cancel" class="btn btn-sm">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="plan-list"></div>
|
||||||
|
|
||||||
|
<div id="planner-info-panel" class="planner-info-panel"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Plan detail -->
|
||||||
|
<div id="plan-detail-panel">
|
||||||
|
<div id="planner-no-plan" class="empty">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<h3>Kein Plan ausgewählt</h3>
|
||||||
|
<p style="font-size:13px;color:var(--t3);margin-top:6px">Erstelle einen neuen Plan oder wähle einen bestehenden aus</p>
|
||||||
|
</div>
|
||||||
|
<div id="plan-content" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -3,6 +3,7 @@
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button class="tab active" data-tab="report">⚔ Report</button>
|
<button class="tab active" data-tab="report">⚔ Report</button>
|
||||||
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
||||||
|
<button class="tab" data-tab="planner">📋 Planer</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="topbar-user">Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?></div>
|
<div class="topbar-user">Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?></div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user