forked from xziino/ff14-mitigator
Compare commits
No commits in common. "27b9b0785e2f6fd58b4b4bc0c9288fc525c3d7e2" and "d8ff50eeefc0110f513fc35f7b2a41d15980cb72" have entirely different histories.
27b9b0785e
...
d8ff50eeef
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
.claude/
|
.claude/
|
||||||
debug/
|
debug/
|
||||||
fflogs-schema.json
|
fflogs-schema.json
|
||||||
config.php
|
|
||||||
|
|||||||
@ -42,7 +42,6 @@ query GetReportData($reportCode: String!) {
|
|||||||
endTime
|
endTime
|
||||||
fights {
|
fights {
|
||||||
id
|
id
|
||||||
encounterID
|
|
||||||
name
|
name
|
||||||
startTime
|
startTime
|
||||||
endTime
|
endTime
|
||||||
@ -76,7 +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);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ch = curl_init(localized_graphql_uri($language));
|
// Fight names must be stable regardless of language — always use the English endpoint.
|
||||||
|
// 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: ' . ($language === 'jp' ? 'ja' : $language),
|
|
||||||
],
|
],
|
||||||
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
<?php
|
|
||||||
ini_set('display_errors', '0');
|
|
||||||
require_once __DIR__ . '/../config.php';
|
|
||||||
session_start_safe();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit; }
|
|
||||||
if (empty($_SESSION['access_token'])) { echo json_encode(['reauth' => true]); exit; }
|
|
||||||
if (($_SESSION['token_expires'] ?? 0) <= time()) { echo json_encode(['reauth' => true]); exit; }
|
|
||||||
|
|
||||||
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
|
|
||||||
$fightId = (int)($_POST['fight_id'] ?? 0);
|
|
||||||
$startTime = (float)($_POST['start_time'] ?? 0);
|
|
||||||
$endTime = (float)($_POST['end_time'] ?? 0);
|
|
||||||
|
|
||||||
if (!$reportCode || !$fightId || !$endTime) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'Missing params']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $_SESSION['access_token'];
|
|
||||||
|
|
||||||
function pl_gql(string $query): array {
|
|
||||||
global $token;
|
|
||||||
$ch = curl_init(GRAPHQL_URI);
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_POST => true,
|
|
||||||
CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
|
|
||||||
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
|
||||||
]);
|
|
||||||
$body = curl_exec($ch);
|
|
||||||
$err = curl_error($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
if ($err) return ['_curl_error' => $err];
|
|
||||||
if ($code === 401) return ['_reauth' => true];
|
|
||||||
return json_decode($body, true) ?? ['_parse_error' => true];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single query: playerDetails + first page of DamageTaken with resources to get maxHp
|
|
||||||
$result = pl_gql(<<<GQL
|
|
||||||
{
|
|
||||||
reportData {
|
|
||||||
report(code: "$reportCode") {
|
|
||||||
playerDetails(fightIDs: [$fightId])
|
|
||||||
events(
|
|
||||||
fightIDs: [$fightId],
|
|
||||||
dataType: DamageTaken,
|
|
||||||
startTime: $startTime,
|
|
||||||
endTime: $endTime,
|
|
||||||
includeResources: true
|
|
||||||
) {
|
|
||||||
data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GQL);
|
|
||||||
|
|
||||||
if (isset($result['_reauth'])) { echo json_encode(['reauth' => true]); exit; }
|
|
||||||
if (isset($result['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $result['_curl_error']]); exit; }
|
|
||||||
|
|
||||||
// Parse player details: id → {name, type, role, maxHp}
|
|
||||||
$pdRaw = $result['data']['reportData']['report']['playerDetails'] ?? null;
|
|
||||||
$pdParsed = is_string($pdRaw) ? json_decode($pdRaw, true) : $pdRaw;
|
|
||||||
$pdGroups = $pdParsed['data']['playerDetails'] ?? [];
|
|
||||||
|
|
||||||
$players = [];
|
|
||||||
$roleMap = ['tanks' => 'tank', 'healers' => 'healer', 'dps' => 'dps'];
|
|
||||||
foreach ($roleMap as $group => $role) {
|
|
||||||
foreach ($pdGroups[$group] ?? [] as $p) {
|
|
||||||
$players[(int)$p['id']] = [
|
|
||||||
'name' => $p['name'],
|
|
||||||
'type' => $p['type'] ?? '',
|
|
||||||
'role' => $role,
|
|
||||||
'maxHp' => 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract maxHp from first damage events (skip DoT ticks — they may lack resources)
|
|
||||||
foreach ($result['data']['reportData']['report']['events']['data'] ?? [] as $ev) {
|
|
||||||
if ($ev['tick'] ?? false) continue;
|
|
||||||
$tid = (int)($ev['targetID'] ?? 0);
|
|
||||||
$maxHp = (int)($ev['targetResources']['maxHitPoints'] ?? 0);
|
|
||||||
if (isset($players[$tid]) && $players[$tid]['maxHp'] === 0 && $maxHp > 0) {
|
|
||||||
$players[$tid]['maxHp'] = $maxHp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode(['players' => array_values($players)]);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -80,30 +80,6 @@ select option { background: var(--bg2); }
|
|||||||
|
|
||||||
.btn-sm { padding: 5px 13px; font-size: 13px; }
|
.btn-sm { padding: 5px 13px; font-size: 13px; }
|
||||||
|
|
||||||
/* ── Export choice dropdown ─────────────────────────────────────────────────── */
|
|
||||||
.export-choice-menu {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg2);
|
|
||||||
border: 1px solid var(--bg3);
|
|
||||||
border-radius: var(--r);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
.export-choice-item {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 18px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--t1);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.export-choice-item:hover { background: var(--bg3); color: var(--gold); }
|
|
||||||
|
|
||||||
/* ── Stats row ──────────────────────────────────────────────────────────────── */
|
/* ── Stats row ──────────────────────────────────────────────────────────────── */
|
||||||
.stats-row {
|
.stats-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -159,63 +159,6 @@
|
|||||||
.job-slot--healer select { border-left-color: var(--green); }
|
.job-slot--healer select { border-left-color: var(--green); }
|
||||||
.job-slot--dps select { border-left-color: rgba(200,168,75,.5); }
|
.job-slot--dps select { border-left-color: rgba(200,168,75,.5); }
|
||||||
|
|
||||||
/* ── Job Slot Player Names ───────────────────────────────────────────────────── */
|
|
||||||
.job-slot-name {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--t2);
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 3px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Name Import Modal ───────────────────────────────────────────────────────── */
|
|
||||||
.name-import-input-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.name-import-input-row input { flex: 1; }
|
|
||||||
|
|
||||||
.name-import-preview {
|
|
||||||
background: var(--bg2);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--r);
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-import-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.name-import-row:last-child { border-bottom: none; }
|
|
||||||
|
|
||||||
.name-import-name {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--t1);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-import-name--none {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--t3);
|
|
||||||
font-style: italic;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-import-disambig {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
|
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
|
||||||
.mechanic-card {
|
.mechanic-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
348
js/analysis.js
348
js/analysis.js
@ -54,63 +54,6 @@
|
|||||||
'Pictomancer': 'PCT', 'BlueMage': 'BLU',
|
'Pictomancer': 'PCT', 'BlueMage': 'BLU',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ability name → jobs that can provide it (for job-based ref comparison)
|
|
||||||
const ABILITY_JOBS = {
|
|
||||||
'Passage of Arms': ['PLD'],
|
|
||||||
'Divine Veil': ['PLD'],
|
|
||||||
'Guardian': ['PLD'],
|
|
||||||
'Reprisal': ['PLD', 'WAR', 'DRK', 'GNB'],
|
|
||||||
'Shake It Off': ['WAR'],
|
|
||||||
'Bloodwhetting': ['WAR'],
|
|
||||||
'Dark Missionary': ['DRK'],
|
|
||||||
'Heart of Light': ['GNB'],
|
|
||||||
'Temperance': ['WHM'],
|
|
||||||
'Divine Benison': ['WHM'],
|
|
||||||
'Divine Caress': ['WHM'],
|
|
||||||
'Sacred Soil': ['SCH'],
|
|
||||||
'Expedient': ['SCH'],
|
|
||||||
'Fey Illumination': ['SCH'],
|
|
||||||
'Galvanize': ['SCH'],
|
|
||||||
'Seraphic Veil': ['SCH'],
|
|
||||||
'Catalyze': ['SCH'],
|
|
||||||
'Collective Unconscious': ['AST'],
|
|
||||||
'Neutral Sect': ['AST'],
|
|
||||||
'Intersection': ['AST'],
|
|
||||||
'the Spire': ['AST'],
|
|
||||||
'Kerachole': ['SGE'],
|
|
||||||
'Holos': ['SGE'],
|
|
||||||
'Holosakos': ['SGE'],
|
|
||||||
'Panhaima': ['SGE'],
|
|
||||||
'Eukrasian Prognosis': ['SGE'],
|
|
||||||
'Eukrasian Prognosis II': ['SGE'],
|
|
||||||
'Eukrasian Diagnosis': ['SGE'],
|
|
||||||
'Differential Diagnosis': ['SGE'],
|
|
||||||
'Haima': ['SGE'],
|
|
||||||
'Troubadour': ['BRD'],
|
|
||||||
'Tactician': ['MCH'],
|
|
||||||
'Shield Samba': ['DNC'],
|
|
||||||
'Improvised Finish': ['DNC'],
|
|
||||||
'Feint': ['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR'],
|
|
||||||
'Addle': ['SCH', 'SGE', 'BLM', 'SMN', 'RDM', 'PCT'],
|
|
||||||
'Radiant Aegis': ['SMN'],
|
|
||||||
'Magick Barrier': ['RDM'],
|
|
||||||
'Tempera Coat': ['PCT'],
|
|
||||||
'Tempera Grassa': ['PCT'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Deduplicated list of all mitigations across all targets of a ref event
|
|
||||||
function collectRefMitigs(refEvent) {
|
|
||||||
if (!refEvent) return [];
|
|
||||||
const seen = new Set(), result = [];
|
|
||||||
for (const t of refEvent.targets ?? []) {
|
|
||||||
for (const m of (t.mitigations ?? [])) {
|
|
||||||
const k = m.key ?? m.name;
|
|
||||||
if (!seen.has(k)) { seen.add(k); result.push(m); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abbr(type) {
|
function abbr(type) {
|
||||||
return JOB_ABBR[type] ?? type.slice(0, 3).toUpperCase();
|
return JOB_ABBR[type] ?? type.slice(0, 3).toUpperCase();
|
||||||
}
|
}
|
||||||
@ -138,24 +81,13 @@
|
|||||||
return String(name ?? '').trim().toLowerCase();
|
return String(name ?? '').trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fightEncounterId(fight) {
|
function currentFightName() {
|
||||||
return parseInt(fight?.encounterID ?? fight?.encounterId ?? 0, 10) || 0;
|
const fight = (window.App?.fights ?? []).find(f => f.id === window.App?.fightId);
|
||||||
|
return normalizeFightName(fight?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentFight() {
|
function isSameFightName(fight) {
|
||||||
return (window.App?.fights ?? []).find(f => f.id === window.App?.fightId) ?? null;
|
const name = currentFightName();
|
||||||
}
|
|
||||||
|
|
||||||
function isSameEncounter(fight) {
|
|
||||||
const selectedFight = currentFight();
|
|
||||||
const selectedEncounterId = fightEncounterId(selectedFight);
|
|
||||||
const encounterId = fightEncounterId(fight);
|
|
||||||
|
|
||||||
if (selectedEncounterId && encounterId) {
|
|
||||||
return encounterId === selectedEncounterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = normalizeFightName(selectedFight?.name);
|
|
||||||
return name !== '' && normalizeFightName(fight?.name) === name;
|
return name !== '' && normalizeFightName(fight?.name) === name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +104,6 @@
|
|||||||
let extFights = [];
|
let extFights = [];
|
||||||
let extReportCode = '';
|
let extReportCode = '';
|
||||||
let mitigationNames = {};
|
let mitigationNames = {};
|
||||||
let planRefId = '';
|
|
||||||
|
|
||||||
// ── Player grid ──────────────────────────────────────────────────────────
|
// ── Player grid ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -313,11 +244,8 @@
|
|||||||
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
|
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
|
||||||
if (!fight) return;
|
if (!fight) return;
|
||||||
|
|
||||||
// Clear ext-report and plan selections
|
// Clear ext-report selection
|
||||||
refExtFightSelect.value = '';
|
refExtFightSelect.value = '';
|
||||||
planRefId = '';
|
|
||||||
refPlanSelect.value = '';
|
|
||||||
refPlanPanel.style.display = 'none';
|
|
||||||
|
|
||||||
refFightSelect.disabled = true;
|
refFightSelect.disabled = true;
|
||||||
try {
|
try {
|
||||||
@ -336,7 +264,7 @@
|
|||||||
if (!json.error && !json.reauth) {
|
if (!json.error && !json.reauth) {
|
||||||
refEvents = json.aoe_events ?? [];
|
refEvents = json.aoe_events ?? [];
|
||||||
refFightStart = json.fight_start ?? fight.startTime;
|
refFightStart = json.fight_start ?? fight.startTime;
|
||||||
refPlayers = json.players ?? [];
|
refPlayers = [];
|
||||||
window.App.setUrlState?.({
|
window.App.setUrlState?.({
|
||||||
compareReportCode: '',
|
compareReportCode: '',
|
||||||
compareFightId: refId,
|
compareFightId: refId,
|
||||||
@ -352,7 +280,7 @@
|
|||||||
let allSameReportFights = [];
|
let allSameReportFights = [];
|
||||||
|
|
||||||
function populateRefFightSelect() {
|
function populateRefFightSelect() {
|
||||||
const visible = allSameReportFights.filter(f => f.id !== window.App.fightId && isSameEncounter(f));
|
const visible = allSameReportFights.filter(f => f.id !== window.App.fightId && isSameFightName(f));
|
||||||
refFightSelect.innerHTML = '<option value="">Kein Vergleich</option>';
|
refFightSelect.innerHTML = '<option value="">Kein Vergleich</option>';
|
||||||
visible.forEach(f => {
|
visible.forEach(f => {
|
||||||
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
||||||
@ -421,7 +349,7 @@
|
|||||||
extReportCode = code;
|
extReportCode = code;
|
||||||
updateRefFflogsLink();
|
updateRefFflogsLink();
|
||||||
|
|
||||||
const visibleExt = fights.filter(isSameEncounter);
|
const visibleExt = fights.filter(isSameFightName);
|
||||||
refExtFightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
refExtFightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
||||||
visibleExt.forEach(f => {
|
visibleExt.forEach(f => {
|
||||||
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
||||||
@ -461,11 +389,8 @@
|
|||||||
const fight = extFights.find(f => f.id === refId);
|
const fight = extFights.find(f => f.id === refId);
|
||||||
if (!fight) return;
|
if (!fight) return;
|
||||||
|
|
||||||
// Clear same-report and plan selections
|
// Clear same-report selection
|
||||||
refFightSelect.value = '';
|
refFightSelect.value = '';
|
||||||
planRefId = '';
|
|
||||||
refPlanSelect.value = '';
|
|
||||||
refPlanPanel.style.display = 'none';
|
|
||||||
|
|
||||||
refExtFightSelect.disabled = true;
|
refExtFightSelect.disabled = true;
|
||||||
try {
|
try {
|
||||||
@ -503,121 +428,6 @@
|
|||||||
await loadExternalCompare(refId);
|
await loadExternalCompare(refId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Plan as reference ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const refPlanToggle = document.getElementById('ref-plan-toggle');
|
|
||||||
const refPlanPanel = document.getElementById('ref-plan-panel');
|
|
||||||
const refPlanSelect = document.getElementById('ref-plan-select');
|
|
||||||
|
|
||||||
const PLAN_JOB_ROLE = {
|
|
||||||
'PLD': 'tank', 'WAR': 'tank', 'DRK': 'tank', 'GNB': 'tank',
|
|
||||||
'WHM': 'healer', 'SCH': 'healer', 'AST': 'healer', 'SGE': 'healer',
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadPlansForRef() {
|
|
||||||
try { return JSON.parse(localStorage.getItem('ff14-planner-plans') || '[]'); }
|
|
||||||
catch { return []; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function planToRefEvents(plan) {
|
|
||||||
const roster = plan.playerRoster ?? [];
|
|
||||||
const jobComp = plan.jobComposition ?? [];
|
|
||||||
const fightStart = plan.source?.fightStart ?? 0;
|
|
||||||
const mitigNames = plan.mitigationNames ?? {};
|
|
||||||
|
|
||||||
const players = jobComp.map((job, i) => ({
|
|
||||||
job,
|
|
||||||
name: roster[i]?.name ?? '',
|
|
||||||
role: PLAN_JOB_ROLE[job] ?? 'dps',
|
|
||||||
})).filter(p => p.name && p.job);
|
|
||||||
|
|
||||||
return plan.mechanics.map(m => {
|
|
||||||
const mitigations = (m.assignments ?? []).map(a => ({
|
|
||||||
key: a.ability,
|
|
||||||
name: a.abilityName || mitigNames[a.ability] || a.ability,
|
|
||||||
buffType: a.buffType,
|
|
||||||
dr: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const targets = players.map(p => ({
|
|
||||||
id: 0,
|
|
||||||
name: p.name,
|
|
||||||
type: p.job,
|
|
||||||
role: p.role,
|
|
||||||
amount: 0,
|
|
||||||
absorbed: 0,
|
|
||||||
overkill: 0,
|
|
||||||
hp: 0,
|
|
||||||
maxHp: 0,
|
|
||||||
unmitigatedAmount: 0,
|
|
||||||
mitigations,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
abilityName: m.name,
|
|
||||||
abilityId: m.abilityId ?? 0,
|
|
||||||
timestamp: fightStart + m.timestamp,
|
|
||||||
totalDamage: 0,
|
|
||||||
targets,
|
|
||||||
isPlanRef: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateRefPlanSelect() {
|
|
||||||
const plans = loadPlansForRef();
|
|
||||||
refPlanSelect.innerHTML = '<option value="">— Plan auswählen —</option>';
|
|
||||||
plans.forEach(p => {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = p.id;
|
|
||||||
opt.textContent = `${p.name} (${p.mechanics.length} Mechaniken)`;
|
|
||||||
refPlanSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
refPlanSelect.value = planRefId || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
refPlanToggle.addEventListener('click', () => {
|
|
||||||
const hidden = refPlanPanel.style.display === 'none';
|
|
||||||
refPlanPanel.style.display = hidden ? '' : 'none';
|
|
||||||
if (hidden) populateRefPlanSelect();
|
|
||||||
});
|
|
||||||
|
|
||||||
refPlanSelect.addEventListener('change', () => {
|
|
||||||
const id = refPlanSelect.value;
|
|
||||||
|
|
||||||
// Clear other ref sources
|
|
||||||
refFightSelect.value = '';
|
|
||||||
refExtFightSelect.value = '';
|
|
||||||
updateRefFflogsLink(0);
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
planRefId = '';
|
|
||||||
refEvents = [];
|
|
||||||
refFightStart = 0;
|
|
||||||
refPlayers = [];
|
|
||||||
renderRefPlayers();
|
|
||||||
renderTimeline(lastEvents, lastFightStart);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = loadPlansForRef().find(p => p.id === id);
|
|
||||||
if (!plan) return;
|
|
||||||
|
|
||||||
planRefId = id;
|
|
||||||
refEvents = planToRefEvents(plan);
|
|
||||||
refFightStart = plan.source?.fightStart ?? 0;
|
|
||||||
refPlayers = (plan.jobComposition ?? [])
|
|
||||||
.map((job, i) => {
|
|
||||||
const name = plan.playerRoster?.[i]?.name ?? '';
|
|
||||||
if (!name || !job) return null;
|
|
||||||
return { name, type: job, role: PLAN_JOB_ROLE[job] ?? 'dps' };
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
renderRefPlayers();
|
|
||||||
renderTimeline(lastEvents, lastFightStart);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Timeline rendering ────────────────────────────────────────────────────
|
// ── Timeline rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderTimeline(events, fightStart) {
|
function renderTimeline(events, fightStart) {
|
||||||
@ -631,8 +441,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentFightJobSet = new Set(currentPlayers.map(p => JOB_ABBR[p.type]).filter(Boolean));
|
|
||||||
|
|
||||||
// Build reference index: abilityName → [events in order]
|
// Build reference index: abilityName → [events in order]
|
||||||
const refIndex = {};
|
const refIndex = {};
|
||||||
for (const ev of refEvents) {
|
for (const ev of refEvents) {
|
||||||
@ -647,11 +455,6 @@
|
|||||||
const occ = abilityOccurrence[ev.abilityName] ?? 0;
|
const occ = abilityOccurrence[ev.abilityName] ?? 0;
|
||||||
abilityOccurrence[ev.abilityName] = occ + 1;
|
abilityOccurrence[ev.abilityName] = occ + 1;
|
||||||
const refEv = refEvents.length ? (refIndex[ev.abilityName]?.[occ] ?? null) : null;
|
const refEv = refEvents.length ? (refIndex[ev.abilityName]?.[occ] ?? null) : null;
|
||||||
const allRefMitigs = collectRefMitigs(refEv);
|
|
||||||
const currentEventMitigKeys = new Set();
|
|
||||||
for (const t of ev.targets) {
|
|
||||||
for (const m of (t.mitigations ?? [])) currentEventMitigKeys.add(m.key ?? m.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleTargets = ev.targets.filter(t =>
|
const visibleTargets = ev.targets.filter(t =>
|
||||||
!hiddenPlayers.has(t.id) &&
|
!hiddenPlayers.has(t.id) &&
|
||||||
@ -673,11 +476,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const eventMissingDebuffs = refEv
|
const eventMissingDebuffs = refEv
|
||||||
? allRefMitigs.filter(m => {
|
? (refEv.targets[0]?.mitigations ?? []).filter(m => m.buffType === 'debuff' && !seenDebuffKeys.has(m.key ?? m.name))
|
||||||
if (m.buffType !== 'debuff' || seenDebuffKeys.has(m.key ?? m.name)) return false;
|
|
||||||
const jobs = ABILITY_JOBS[m.key] ?? ABILITY_JOBS[m.name];
|
|
||||||
return jobs ? jobs.some(j => currentFightJobSet.has(j)) : false;
|
|
||||||
})
|
|
||||||
: [];
|
: [];
|
||||||
const debuffIconsHtml = [
|
const debuffIconsHtml = [
|
||||||
...eventDebuffs.map(m => ({ ...m, missing: false })),
|
...eventDebuffs.map(m => ({ ...m, missing: false })),
|
||||||
@ -708,6 +507,12 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
})() : '';
|
})() : '';
|
||||||
|
|
||||||
|
const currentMitigKeys = new Set((t.mitigations ?? []).map(m => m.key ?? m.name));
|
||||||
|
const refTarget = refEv?.targets?.find(rt => rt.name === t.name);
|
||||||
|
const missingMitigs = refTarget
|
||||||
|
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigKeys.has(m.key ?? m.name))
|
||||||
|
: [];
|
||||||
|
|
||||||
// DR buff icons (shown below player box)
|
// DR buff icons (shown below player box)
|
||||||
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
||||||
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
|
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
|
||||||
@ -718,7 +523,13 @@
|
|||||||
|
|
||||||
// Shield tooltip on absorbed value
|
// Shield tooltip on absorbed value
|
||||||
const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
|
const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
|
||||||
const shieldLines = activeShields.map(s => s.name);
|
const missingShields = refTarget
|
||||||
|
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigKeys.has(m.key ?? m.name))
|
||||||
|
: [];
|
||||||
|
const shieldLines = [
|
||||||
|
...activeShields.map(s => s.name),
|
||||||
|
...missingShields.map(s => `[fehlt: ${s.name}]`),
|
||||||
|
];
|
||||||
const shieldTitle = shieldLines.length ? shieldLines.join('\n') : null;
|
const shieldTitle = shieldLines.length ? shieldLines.join('\n') : null;
|
||||||
|
|
||||||
const dead = t.hp === 0 && t.maxHp > 0;
|
const dead = t.hp === 0 && t.maxHp > 0;
|
||||||
@ -763,25 +574,22 @@
|
|||||||
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
|
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const isPlanRef = !!refEv.isPlanRef;
|
|
||||||
|
|
||||||
const refCards = refVisible.map(t => {
|
const refCards = refVisible.map(t => {
|
||||||
const curr = currentByName[t.name];
|
const curr = currentByName[t.name];
|
||||||
const diff = (!isPlanRef && curr) ? curr.amount - t.amount : 0;
|
const diff = curr ? curr.amount - t.amount : 0;
|
||||||
const dead = !isPlanRef && t.hp === 0 && t.maxHp > 0;
|
const dead = t.hp === 0 && t.maxHp > 0;
|
||||||
|
|
||||||
const deltaHtml = diff !== 0
|
const deltaHtml = diff !== 0
|
||||||
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
|
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const currMitigKeys = new Set((curr?.mitigations ?? []).map(m => m.key ?? m.name));
|
||||||
|
|
||||||
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
||||||
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
|
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
|
||||||
if (!iconSrc) return '';
|
if (!iconSrc) return '';
|
||||||
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
||||||
const k = m.key ?? m.name;
|
const missing = !currMitigKeys.has(m.key ?? m.name);
|
||||||
const jobs = ABILITY_JOBS[k] ?? ABILITY_JOBS[m.name];
|
|
||||||
const currentGroupHasJob = jobs ? jobs.some(j => currentFightJobSet.has(j)) : false;
|
|
||||||
const missing = currentGroupHasJob && !currentEventMitigKeys.has(k);
|
|
||||||
const cls = missing ? ' aoe-buff-ref-unique' : '';
|
const cls = missing ? ' aoe-buff-ref-unique' : '';
|
||||||
const titleSufx = missing ? ' (fehlt im aktuellen Pull)' : '';
|
const titleSufx = missing ? ' (fehlt im aktuellen Pull)' : '';
|
||||||
return `<img class="aoe-target-buff-icon${cls}" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}${titleSufx}">`;
|
return `<img class="aoe-target-buff-icon${cls}" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}${titleSufx}">`;
|
||||||
@ -789,19 +597,9 @@
|
|||||||
|
|
||||||
const refShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
|
const refShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
|
||||||
const refShieldTitle = refShields.length
|
const refShieldTitle = refShields.length
|
||||||
? refShields.map(s => {
|
? refShields.map(s => currMitigKeys.has(s.key ?? s.name) ? s.name : `${s.name} [fehlt im aktuellen Pull]`).join('\n')
|
||||||
const k = s.key ?? s.name;
|
|
||||||
const jobs = ABILITY_JOBS[k];
|
|
||||||
const currentGroupHasJob = jobs ? jobs.some(j => currentFightJobSet.has(j)) : false;
|
|
||||||
const isMissing = !isPlanRef && currentGroupHasJob && !currentEventMitigKeys.has(k);
|
|
||||||
return isMissing ? `${s.name} [fehlt im aktuellen Pull]` : s.name;
|
|
||||||
}).join('\n')
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const absorbedHtml = isPlanRef
|
|
||||||
? (refShields.length ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? ''}">Schild</span>` : '')
|
|
||||||
: (t.absorbed > 0 ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? 'Keine erkannten Schilde'}">+${fmtDmg(t.absorbed)}</span>` : '');
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="aoe-target-wrap">
|
<div class="aoe-target-wrap">
|
||||||
<div class="aoe-ref-target${dead ? ' aoe-target--dead' : ''}">
|
<div class="aoe-ref-target${dead ? ' aoe-target--dead' : ''}">
|
||||||
@ -810,21 +608,20 @@
|
|||||||
${deltaHtml}
|
${deltaHtml}
|
||||||
</div>
|
</div>
|
||||||
<span class="aoe-target-name">${t.name}</span>
|
<span class="aoe-target-name">${t.name}</span>
|
||||||
<span class="aoe-target-dmg">${isPlanRef ? '' : fmtDmg(t.amount)}${absorbedHtml}</span>
|
<span class="aoe-target-dmg">${fmtDmg(t.amount)}${t.absorbed > 0 ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? 'Keine erkannten Schilde'}">+${fmtDmg(t.absorbed)}</span>` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
${refMitigIcons ? `<div class="aoe-target-buffs">${refMitigIcons}</div>` : ''}
|
${refMitigIcons ? `<div class="aoe-target-buffs">${refMitigIcons}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const totalDiff = ev.totalDamage - refEv.totalDamage;
|
const totalDiff = ev.totalDamage - refEv.totalDamage;
|
||||||
const totalDelta = (!isPlanRef && totalDiff !== 0)
|
const totalDelta = totalDiff !== 0
|
||||||
? `<span class="${totalDiff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${totalDiff > 0 ? '+' : ''}${fmtDmg(totalDiff)}</span>`
|
? `<span class="${totalDiff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${totalDiff > 0 ? '+' : ''}${fmtDmg(totalDiff)}</span>`
|
||||||
: '';
|
: '';
|
||||||
const refLabel = isPlanRef ? 'PLAN' : `REF ${fmtDmg(refEv.totalDamage)} ${totalDelta}`;
|
|
||||||
|
|
||||||
refHtml = `
|
refHtml = `
|
||||||
<div class="aoe-ref-row">
|
<div class="aoe-ref-row">
|
||||||
<span class="aoe-ref-label">${refLabel} ${refDebuffIconsHtml}</span>
|
<span class="aoe-ref-label">REF ${fmtDmg(refEv.totalDamage)} ${totalDelta} ${refDebuffIconsHtml}</span>
|
||||||
<div class="aoe-targets">${refCards}</div>
|
<div class="aoe-targets">${refCards}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@ -927,42 +724,6 @@
|
|||||||
mitigationNames,
|
mitigationNames,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
exportRefForPlanner() {
|
|
||||||
const sameReportId = parseInt(refFightSelect.value, 10);
|
|
||||||
const extId = parseInt(refExtFightSelect.value, 10);
|
|
||||||
let fight = null, reportCode = '', fightId = 0;
|
|
||||||
if (sameReportId) {
|
|
||||||
fight = allSameReportFights.find(f => f.id === sameReportId);
|
|
||||||
reportCode = window.App?.reportCode ?? '';
|
|
||||||
fightId = sameReportId;
|
|
||||||
} else if (extId) {
|
|
||||||
fight = extFights.find(f => f.id === extId);
|
|
||||||
reportCode = extReportCode;
|
|
||||||
fightId = extId;
|
|
||||||
}
|
|
||||||
const transitions = fight?.phaseTransitions ?? [];
|
|
||||||
const phases = transitions.length === 0 ? [] : [
|
|
||||||
{ id: 0, name: 'Ganzer Fight', startTime: fight.startTime, endTime: fight.endTime },
|
|
||||||
...transitions.map((t, i) => ({
|
|
||||||
id: t.id,
|
|
||||||
name: `Phase ${t.id}`,
|
|
||||||
startTime: t.startTime,
|
|
||||||
endTime: transitions[i + 1]?.startTime ?? fight.endTime,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
aoeEvents: refEvents,
|
|
||||||
fightStart: refFightStart,
|
|
||||||
phases,
|
|
||||||
players: refPlayers,
|
|
||||||
fightName: fight?.name ?? 'Referenz-Fight',
|
|
||||||
reportCode,
|
|
||||||
fightId,
|
|
||||||
fightEnd: fight?.endTime ?? 0,
|
|
||||||
mitigationNames,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hasRefExport() { return refEvents.length > 0 && !planRefId; },
|
|
||||||
reset() {
|
reset() {
|
||||||
lastFightId = null;
|
lastFightId = null;
|
||||||
refEvents = [];
|
refEvents = [];
|
||||||
@ -971,7 +732,6 @@
|
|||||||
extFights = [];
|
extFights = [];
|
||||||
extReportCode = '';
|
extReportCode = '';
|
||||||
mitigationNames = {};
|
mitigationNames = {};
|
||||||
planRefId = '';
|
|
||||||
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';
|
||||||
@ -980,52 +740,12 @@
|
|||||||
refFflogsLink.style.display = 'none';
|
refFflogsLink.style.display = 'none';
|
||||||
refFflogsLink.href = '#';
|
refFflogsLink.href = '#';
|
||||||
refExtPanel.style.display = 'none';
|
refExtPanel.style.display = 'none';
|
||||||
refPlanPanel.style.display = 'none';
|
|
||||||
refPlanSelect.value = '';
|
|
||||||
const exportBtn = document.getElementById('export-to-planner-btn');
|
const exportBtn = document.getElementById('export-to-planner-btn');
|
||||||
if (exportBtn) exportBtn.style.display = 'none';
|
if (exportBtn) exportBtn.style.display = 'none';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('export-to-planner-btn')?.addEventListener('click', (e) => {
|
document.getElementById('export-to-planner-btn')?.addEventListener('click', () => {
|
||||||
if (!refEvents.length) {
|
|
||||||
window.plannerTab?.showImportModal(window.analysisTab.exportForPlanner());
|
window.plannerTab?.showImportModal(window.analysisTab.exportForPlanner());
|
||||||
return;
|
|
||||||
}
|
|
||||||
showExportChoiceMenu(e.currentTarget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function showExportChoiceMenu(anchor) {
|
|
||||||
document.getElementById('export-choice-menu')?.remove();
|
|
||||||
const menu = document.createElement('div');
|
|
||||||
menu.id = 'export-choice-menu';
|
|
||||||
menu.className = 'export-choice-menu';
|
|
||||||
|
|
||||||
[
|
|
||||||
{ label: 'Aktueller Fight', fn: () => window.analysisTab.exportForPlanner() },
|
|
||||||
{ label: 'Referenz-Fight', fn: () => window.analysisTab.exportRefForPlanner() },
|
|
||||||
].forEach(({ label, fn }) => {
|
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.className = 'export-choice-item';
|
|
||||||
btn.textContent = label;
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
menu.remove();
|
|
||||||
window.plannerTab?.showImportModal(fn());
|
|
||||||
});
|
|
||||||
menu.appendChild(btn);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(menu);
|
|
||||||
const rect = anchor.getBoundingClientRect();
|
|
||||||
menu.style.top = (rect.bottom + 4) + 'px';
|
|
||||||
menu.style.right = (window.innerWidth - rect.right) + 'px';
|
|
||||||
|
|
||||||
const close = (ev) => {
|
|
||||||
if (!menu.contains(ev.target) && ev.target !== anchor) {
|
|
||||||
menu.remove();
|
|
||||||
document.removeEventListener('click', close, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(() => document.addEventListener('click', close, true), 0);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
228
js/planner.js
228
js/planner.js
@ -325,10 +325,7 @@ function renderPlanDetail(plan) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card section-gap">
|
<div class="card section-gap">
|
||||||
<div class="card-title-row">
|
|
||||||
<div class="card-title">Jobaufstellung</div>
|
<div class="card-title">Jobaufstellung</div>
|
||||||
<button id="name-import-open-btn" class="btn btn-sm">Namen + Job Import</button>
|
|
||||||
</div>
|
|
||||||
<div class="job-slots-grid" id="job-slots-grid">
|
<div class="job-slots-grid" id="job-slots-grid">
|
||||||
${renderJobSlotsHtml(plan)}
|
${renderJobSlotsHtml(plan)}
|
||||||
</div>
|
</div>
|
||||||
@ -348,9 +345,6 @@ function renderPlanDetail(plan) {
|
|||||||
startRename(plan.id, plan.name);
|
startRename(plan.id, plan.name);
|
||||||
});
|
});
|
||||||
initJobSlots(plan.id);
|
initJobSlots(plan.id);
|
||||||
document.getElementById('name-import-open-btn')?.addEventListener('click', () => {
|
|
||||||
showNameImportModal(plan.id);
|
|
||||||
});
|
|
||||||
initMechanicClicks(plan.id);
|
initMechanicClicks(plan.id);
|
||||||
renderInfoPanel(plan);
|
renderInfoPanel(plan);
|
||||||
}
|
}
|
||||||
@ -423,11 +417,9 @@ function renderMechanicListHtml(plan) {
|
|||||||
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderJobSlotsHtml(plan) {
|
function renderJobSlotsHtml(plan) {
|
||||||
const roster = plan.playerRoster ?? [];
|
|
||||||
return Array.from({ length: 8 }, (_, i) => {
|
return Array.from({ length: 8 }, (_, i) => {
|
||||||
const job = plan.jobComposition[i] ?? '';
|
const job = plan.jobComposition[i] ?? '';
|
||||||
const role = JOB_ROLE[job] ?? '';
|
const role = JOB_ROLE[job] ?? '';
|
||||||
const playerName = roster[i]?.name ?? '';
|
|
||||||
return `
|
return `
|
||||||
<div class="job-slot${role ? ` job-slot--${role}` : ''}">
|
<div class="job-slot${role ? ` job-slot--${role}` : ''}">
|
||||||
<select class="job-slot-select" data-idx="${i}">
|
<select class="job-slot-select" data-idx="${i}">
|
||||||
@ -438,7 +430,6 @@ function renderJobSlotsHtml(plan) {
|
|||||||
).join('')}</optgroup>`
|
).join('')}</optgroup>`
|
||||||
).join('')}
|
).join('')}
|
||||||
</select>
|
</select>
|
||||||
${playerName ? `<div class="job-slot-name" title="${escHtml(playerName)}">${escHtml(playerName)}</div>` : ''}
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@ -1018,36 +1009,12 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
|||||||
|
|
||||||
// ── Merge + Create plan from import ──────────────────────────────────────────
|
// ── Merge + Create plan from import ──────────────────────────────────────────
|
||||||
|
|
||||||
function buildPlayerRoster(players, aoeEvents) {
|
|
||||||
const maxHpByName = {};
|
|
||||||
for (const ev of aoeEvents ?? []) {
|
|
||||||
for (const t of ev.targets ?? []) {
|
|
||||||
if (t.name && t.maxHp > 0 && !(t.name in maxHpByName)) {
|
|
||||||
maxHpByName[t.name] = t.maxHp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const order = { healer: 0, dps: 1, tank: 2 };
|
|
||||||
const sorted = [...(players ?? [])]
|
|
||||||
.filter(p => JOB_FROM_TYPE[p.type])
|
|
||||||
.sort((a, b) => {
|
|
||||||
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
|
||||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
const roster = sorted.slice(0, 8).map(p => ({
|
|
||||||
name: p.name,
|
|
||||||
maxHp: maxHpByName[p.name] ?? 0,
|
|
||||||
}));
|
|
||||||
while (roster.length < 8) roster.push({ name: '', maxHp: 0 });
|
|
||||||
return roster;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractJobComp(players) {
|
function extractJobComp(players) {
|
||||||
const order = { healer: 0, dps: 1, tank: 2 };
|
const order = { tank: 0, healer: 1, dps: 2 };
|
||||||
const sorted = [...(players ?? [])]
|
const sorted = [...(players ?? [])]
|
||||||
.filter(p => JOB_FROM_TYPE[p.type])
|
.filter(p => JOB_FROM_TYPE[p.type])
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
const roleCmp = (order[a.role] ?? 2) - (order[b.role] ?? 2);
|
||||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
const comp = sorted.map(p => JOB_FROM_TYPE[p.type] ?? '').slice(0, 8);
|
const comp = sorted.map(p => JOB_FROM_TYPE[p.type] ?? '').slice(0, 8);
|
||||||
@ -1067,7 +1034,6 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
|||||||
source,
|
source,
|
||||||
mitigationNames,
|
mitigationNames,
|
||||||
jobComposition: extractJobComp(players),
|
jobComposition: extractJobComp(players),
|
||||||
playerRoster: buildPlayerRoster(players, aoeEvents),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1368,195 +1334,6 @@ function initImportModal() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Name Import Modal ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
let nameImportPlanId = null;
|
|
||||||
let nameImportFights = [];
|
|
||||||
let nameImportReportCode = '';
|
|
||||||
let nameImportMatchData = []; // length-8 array: null | {job, matched:[{name,type,role,maxHp}], selected:0}
|
|
||||||
|
|
||||||
function showNameImportModal(planId) {
|
|
||||||
nameImportPlanId = planId;
|
|
||||||
nameImportFights = [];
|
|
||||||
nameImportReportCode = '';
|
|
||||||
nameImportMatchData = [];
|
|
||||||
|
|
||||||
document.getElementById('name-import-report-input').value = '';
|
|
||||||
document.getElementById('name-import-fight-section').style.display = 'none';
|
|
||||||
document.getElementById('name-import-fight-select').innerHTML = '<option value="">— Fight auswählen —</option>';
|
|
||||||
document.getElementById('name-import-preview').style.display = 'none';
|
|
||||||
document.getElementById('name-import-preview').innerHTML = '';
|
|
||||||
document.getElementById('name-import-confirm-btn').style.display = 'none';
|
|
||||||
|
|
||||||
document.getElementById('planner-name-import-modal').style.display = 'flex';
|
|
||||||
document.getElementById('name-import-report-input').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideNameImportModal() {
|
|
||||||
document.getElementById('planner-name-import-modal').style.display = 'none';
|
|
||||||
nameImportPlanId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshJobSlots(planId) {
|
|
||||||
const plan = getPlan(planId);
|
|
||||||
if (!plan) return;
|
|
||||||
const grid = document.getElementById('job-slots-grid');
|
|
||||||
if (grid) grid.innerHTML = renderJobSlotsHtml(plan);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNameImportPreview(_plan, fetchedPlayers) {
|
|
||||||
const order = { healer: 0, dps: 1, tank: 2 };
|
|
||||||
const sorted = [...fetchedPlayers]
|
|
||||||
.filter(p => JOB_FROM_TYPE[p.type])
|
|
||||||
.sort((a, b) => {
|
|
||||||
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
|
||||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
nameImportMatchData = sorted.slice(0, 8);
|
|
||||||
|
|
||||||
const preview = document.getElementById('name-import-preview');
|
|
||||||
|
|
||||||
const rows = Array.from({ length: 8 }, (_, i) => {
|
|
||||||
const p = sorted[i];
|
|
||||||
if (!p) {
|
|
||||||
return `
|
|
||||||
<div class="name-import-row">
|
|
||||||
<span class="aoe-target-job" style="opacity:0.25">—</span>
|
|
||||||
<span class="name-import-name--none">Leer</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
const job = JOB_FROM_TYPE[p.type] ?? p.type;
|
|
||||||
const role = JOB_ROLE[job] ?? 'dps';
|
|
||||||
return `
|
|
||||||
<div class="name-import-row">
|
|
||||||
<span class="aoe-target-job role-${role}">${escHtml(job)}</span>
|
|
||||||
<span class="name-import-name">${escHtml(p.name)}</span>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
preview.innerHTML = rows;
|
|
||||||
preview.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function initNameImportModal() {
|
|
||||||
const modal = document.getElementById('planner-name-import-modal');
|
|
||||||
const reportInput = document.getElementById('name-import-report-input');
|
|
||||||
const loadBtn = document.getElementById('name-import-load-btn');
|
|
||||||
const fightSection = document.getElementById('name-import-fight-section');
|
|
||||||
const fightSelect = document.getElementById('name-import-fight-select');
|
|
||||||
const preview = document.getElementById('name-import-preview');
|
|
||||||
const confirmBtn = document.getElementById('name-import-confirm-btn');
|
|
||||||
const cancelBtn = document.getElementById('name-import-cancel-btn');
|
|
||||||
if (!modal) return;
|
|
||||||
|
|
||||||
cancelBtn.addEventListener('click', hideNameImportModal);
|
|
||||||
modal.addEventListener('click', e => { if (e.target === modal) hideNameImportModal(); });
|
|
||||||
document.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Escape' && nameImportPlanId) hideNameImportModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
reportInput.addEventListener('input', () => {
|
|
||||||
const match = reportInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/);
|
|
||||||
if (match) reportInput.value = match[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
loadBtn.addEventListener('click', async () => {
|
|
||||||
const code = reportInput.value.trim();
|
|
||||||
if (!code) return;
|
|
||||||
|
|
||||||
loadBtn.disabled = true;
|
|
||||||
loadBtn.textContent = 'Lädt…';
|
|
||||||
fightSection.style.display = 'none';
|
|
||||||
preview.style.display = 'none';
|
|
||||||
confirmBtn.style.display = 'none';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('api/fight.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
body: new URLSearchParams({ report_code: code, language: 'en' }),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; }
|
|
||||||
|
|
||||||
nameImportFights = json?.data?.reportData?.report?.fights ?? [];
|
|
||||||
nameImportReportCode = code;
|
|
||||||
|
|
||||||
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
|
||||||
nameImportFights.forEach(f => {
|
|
||||||
const ms = f.endTime - f.startTime;
|
|
||||||
const dur = `${Math.floor(ms / 60000)}:${String(Math.floor((ms % 60000) / 1000)).padStart(2, '0')}`;
|
|
||||||
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = f.id;
|
|
||||||
opt.textContent = `${f.name} — ${dur} — ${hp}`;
|
|
||||||
fightSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
fightSection.style.display = '';
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
loadBtn.disabled = false;
|
|
||||||
loadBtn.textContent = 'Laden';
|
|
||||||
});
|
|
||||||
|
|
||||||
fightSelect.addEventListener('change', async () => {
|
|
||||||
const fightId = parseInt(fightSelect.value, 10);
|
|
||||||
if (!fightId) {
|
|
||||||
preview.style.display = 'none';
|
|
||||||
confirmBtn.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fight = nameImportFights.find(f => f.id === fightId);
|
|
||||||
if (!fight) return;
|
|
||||||
|
|
||||||
fightSelect.disabled = true;
|
|
||||||
preview.innerHTML = '<div style="padding:10px 12px;color:var(--t3);font-size:13px">Lädt…</div>';
|
|
||||||
preview.style.display = '';
|
|
||||||
confirmBtn.style.display = 'none';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('api/players.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
body: new URLSearchParams({
|
|
||||||
report_code: nameImportReportCode,
|
|
||||||
fight_id: fightId,
|
|
||||||
start_time: fight.startTime,
|
|
||||||
end_time: fight.endTime,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; }
|
|
||||||
if (json.error) { preview.innerHTML = `<div style="padding:10px 12px;color:var(--red);font-size:13px">${escHtml(json.error)}</div>`; return; }
|
|
||||||
|
|
||||||
const plan = getPlan(nameImportPlanId);
|
|
||||||
if (!plan) return;
|
|
||||||
renderNameImportPreview(plan, json.players ?? []);
|
|
||||||
confirmBtn.style.display = '';
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
fightSelect.disabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
confirmBtn.addEventListener('click', () => {
|
|
||||||
if (!nameImportPlanId) return;
|
|
||||||
|
|
||||||
const jobComposition = Array.from({ length: 8 }, (_, i) => {
|
|
||||||
const p = nameImportMatchData[i];
|
|
||||||
return p ? (JOB_FROM_TYPE[p.type] ?? '') : '';
|
|
||||||
});
|
|
||||||
const playerRoster = Array.from({ length: 8 }, (_, i) => {
|
|
||||||
const p = nameImportMatchData[i];
|
|
||||||
return p ? { name: p.name, maxHp: p.maxHp ?? 0 } : { name: '', maxHp: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
updatePlan(nameImportPlanId, { jobComposition, playerRoster });
|
|
||||||
openPlan(nameImportPlanId);
|
|
||||||
hideNameImportModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
||||||
|
|
||||||
window.plannerTab = {
|
window.plannerTab = {
|
||||||
@ -1585,7 +1362,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initNewFolderForm();
|
initNewFolderForm();
|
||||||
initImportModal();
|
initImportModal();
|
||||||
initAbilityModal();
|
initAbilityModal();
|
||||||
initNameImportModal();
|
|
||||||
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
|
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
|
||||||
renderPlanList();
|
renderPlanList();
|
||||||
if (activePlanId && getPlan(activePlanId)) {
|
if (activePlanId && getPlan(activePlanId)) {
|
||||||
|
|||||||
@ -80,35 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name Import Modal (Planner) -->
|
|
||||||
<div id="planner-name-import-modal" class="modal-overlay" style="display:none">
|
|
||||||
<div class="modal-box">
|
|
||||||
<div class="modal-title">Namen importieren</div>
|
|
||||||
|
|
||||||
<div class="modal-section">
|
|
||||||
<div class="modal-label">Report-Code</div>
|
|
||||||
<div class="name-import-input-row">
|
|
||||||
<input type="text" id="name-import-report-input" placeholder="Report-Code oder URL…">
|
|
||||||
<button id="name-import-load-btn" class="btn btn-sm">Laden</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-section" id="name-import-fight-section" style="display:none">
|
|
||||||
<div class="modal-label">Fight</div>
|
|
||||||
<select id="name-import-fight-select">
|
|
||||||
<option value="">— Fight auswählen —</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="name-import-preview" class="name-import-preview" style="display:none"></div>
|
|
||||||
|
|
||||||
<div class="modal-actions" style="margin-top:16px">
|
|
||||||
<button id="name-import-confirm-btn" class="btn btn-gold" style="display:none">Übernehmen</button>
|
|
||||||
<button id="name-import-cancel-btn" class="btn">Abbrechen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ability Assignment Modal -->
|
<!-- Ability Assignment Modal -->
|
||||||
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
|
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
|
||||||
<div class="modal-box ability-modal-box">
|
<div class="modal-box ability-modal-box">
|
||||||
|
|||||||
@ -22,15 +22,6 @@
|
|||||||
<div class="ref-player-label">REF Spieler</div>
|
<div class="ref-player-label">REF Spieler</div>
|
||||||
<div id="ref-player-grid" class="player-grid"></div>
|
<div id="ref-player-grid" class="player-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ref-ext-row">
|
|
||||||
<button id="ref-plan-toggle" class="btn btn-sm">+ Plan als Referenz</button>
|
|
||||||
<div id="ref-plan-panel" style="display:none">
|
|
||||||
<select id="ref-plan-select" class="filter-input">
|
|
||||||
<option value="">— Plan auswählen —</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ref-ext-row">
|
<div class="ref-ext-row">
|
||||||
<button id="ref-ext-toggle" class="btn btn-sm">+ Anderer Report</button>
|
<button id="ref-ext-toggle" class="btn btn-sm">+ Anderer Report</button>
|
||||||
<div id="ref-ext-panel" style="display:none">
|
<div id="ref-ext-panel" style="display:none">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user