diff --git a/css/planner.css b/css/planner.css index 6d28b1a..f23059b 100644 --- a/css/planner.css +++ b/css/planner.css @@ -1240,6 +1240,27 @@ gap: 4px; } +.planner-card-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.cactbot-export-option { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--t2); + font-size: 12px; + white-space: nowrap; +} +.cactbot-export-option input { + width: auto; + margin: 0; +} + .view-toggle-btn { padding: 4px 12px; border: 1px solid var(--border); diff --git a/js/planner.js b/js/planner.js index 39fde44..c0d3279 100644 --- a/js/planner.js +++ b/js/planner.js @@ -461,9 +461,16 @@ function renderPlanDetail(plan) {
Mechaniken
-
- - +
+
+ + +
+ +
@@ -495,6 +502,7 @@ function renderPlanDetail(plan) { initTimeline(plan.id); initMechanicClicks(plan.id); initMySpells(plan.id); + initCactbotExport(plan.id); renderInfoPanel(plan); ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id)); } @@ -555,6 +563,103 @@ function mySpellsPlainText(plan, job) { return lines.join('\n'); } +function cactbotEscape(text) { + return String(text ?? '') + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"'); +} + +function cactbotTime(ms) { + const seconds = Math.max(0, Number(ms) || 0) / 1000; + return seconds.toFixed(1).replace(/\.0$/, ''); +} + +function cactbotExportFilename(plan) { + const base = String(plan?.name || 'mitigation-plan') + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'mitigation-plan'; + return `${base}-mitigations.txt`; +} + +function cactbotAssignmentLabel(assignment, plan) { + const ability = assignmentAbilityName(assignment, plan); + return assignment.job ? `${assignment.job} ${ability}` : ability; +} + +function cactbotFilterHints(plan) { + const jobs = [...new Set((plan.jobComposition ?? []).filter(Boolean))].sort(); + if (!jobs.length) return []; + return ['# Optional job filters - uncomment lines you do not want to see:', ...jobs.map(job => `# hideall ".*${job} .*"`)]; +} + +function cactbotTimelineText(plan, splitEntries = false) { + const byStart = new Map(); + for (const mechanic of visiblePlanMechanics(plan)) { + const planned = sortedAssignments(plannedAssignmentsForMechanic(plan, mechanic)); + if (!planned.length) continue; + + planned.forEach(assignment => { + const start = Number(assignment.sourceStart ?? assignmentStartMs(mechanic, assignment)) || 0; + const startKey = Math.round(start); + if (!byStart.has(startKey)) byStart.set(startKey, { assignments: new Map() }); + const group = byStart.get(startKey); + const assignmentKey = `${assignment.job ?? ''}::${assignment.ability}`; + group.assignments.set(assignmentKey, cactbotAssignmentLabel(assignment, plan)); + }); + } + + const rows = []; + for (const [time, group] of byStart.entries()) { + const assignments = [...group.assignments.values()]; + if (splitEntries) { + assignments.forEach(text => rows.push({ time, text })); + } else { + rows.push({ time, text: assignments.join(', ') }); + } + } + rows.sort((a, b) => a.time - b.time || a.text.localeCompare(b.text)); + const header = [ + `# Exported from FF14 Mitigator`, + `# Plan: ${plan.name}`, + plan.source?.fightName ? `# Fight: ${plan.source.fightName}` : null, + `# Format: cactbot timeline entries`, + ...cactbotFilterHints(plan), + ].filter(Boolean); + + return [ + ...header, + ...rows.map(row => `${cactbotTime(row.time)} "${cactbotEscape(row.text)}"`), + ].join('\n') + '\n'; +} + +function downloadTextFile(filename, text) { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +function initCactbotExport(planId) { + document.getElementById('cactbot-export-btn')?.addEventListener('click', () => { + const plan = getPlan(planId); + if (!plan) return; + const splitEntries = !!document.getElementById('cactbot-export-split')?.checked; + const text = cactbotTimelineText(plan, splitEntries); + if (!text.split('\n').some(line => /^\d/.test(line))) { + alert('Keine Mitigations zum Exportieren gefunden.'); + return; + } + downloadTextFile(cactbotExportFilename(plan), text); + }); +} + function initMySpells(planId) { const viewBtns = document.querySelectorAll('.view-toggle-btn'); const mechList = document.getElementById('mechanic-list');