cactbot export

This commit is contained in:
Akurosia Kamo 2026-05-29 10:41:55 +02:00
parent 76f9baec40
commit 9cd3278b80
2 changed files with 129 additions and 3 deletions

View File

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

View File

@ -461,10 +461,17 @@ function renderPlanDetail(plan) {
<div class="card">
<div class="card-title-row">
<div class="card-title">Mechaniken</div>
<div class="planner-card-actions">
<div class="view-toggle-btns">
<button class="view-toggle-btn active" data-view="mechanics">Mechaniken</button>
<button class="view-toggle-btn" data-view="myspells"> Meine Spells</button>
</div>
<label class="cactbot-export-option">
<input type="checkbox" id="cactbot-export-split">
<span>Separate Entries</span>
</label>
<button id="cactbot-export-btn" class="btn btn-sm" title="Cactbot Timeline exportieren">Cactbot Export</button>
</div>
</div>
<div id="mechanic-list">
${renderMechanicListHtml(plan)}
@ -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');