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');