forked from xziino/ff14-mitigator
add amkis whishes, cactbot timeline updates and biinos easy fix
This commit is contained in:
parent
0a47b126cf
commit
695c0fb99e
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"python-envs.pythonProjects": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"envManager": "ms-python.python:system",
|
||||||
|
"packageManager": "ms-python.python:pip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
230
css/planner.css
230
css/planner.css
@ -993,6 +993,39 @@
|
|||||||
opacity: 0.78;
|
opacity: 0.78;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
height: 20px;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap-fit,
|
||||||
|
.timeline-inactive-gap-late {
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap-fit {
|
||||||
|
left: 0;
|
||||||
|
border-top: 2px dotted rgba(180,190,205,.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap-late {
|
||||||
|
border-top: 2px dotted rgba(224,92,92,.70);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap:hover .timeline-inactive-gap-fit {
|
||||||
|
border-top-color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-inactive-gap:hover .timeline-inactive-gap-late {
|
||||||
|
border-top-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-mitigation img {
|
.timeline-mitigation img {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@ -1233,6 +1266,24 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-menu-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 9px 9px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--t3);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-menu-time input {
|
||||||
|
width: 76px !important;
|
||||||
|
padding: 5px 7px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── View Toggle ─────────────────────────────────────────────────────────── */
|
/* ── View Toggle ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.view-toggle-btns {
|
.view-toggle-btns {
|
||||||
@ -1248,17 +1299,176 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cactbot-export-option {
|
.cactbot-export-modal-box {
|
||||||
display: inline-flex;
|
max-width: 720px;
|
||||||
align-items: center;
|
max-height: min(86vh, 780px);
|
||||||
gap: 5px;
|
overflow-y: auto;
|
||||||
color: var(--t2);
|
padding: 0;
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.cactbot-export-option input {
|
|
||||||
width: auto;
|
.cactbot-modal-header {
|
||||||
margin: 0;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 28px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-modal-header .modal-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-modal-subtitle {
|
||||||
|
color: var(--t3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-export-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 18px 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card,
|
||||||
|
.cactbot-check-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r);
|
||||||
|
background: var(--bg2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card:hover,
|
||||||
|
.cactbot-check-card:hover {
|
||||||
|
border-color: var(--borderem);
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card input,
|
||||||
|
.cactbot-check-card input {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card span,
|
||||||
|
.cactbot-check-card span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card strong,
|
||||||
|
.cactbot-check-card strong {
|
||||||
|
color: var(--t1);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-option-card small,
|
||||||
|
.cactbot-check-card small {
|
||||||
|
color: var(--t3);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 16px 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-field label,
|
||||||
|
.cactbot-section-title {
|
||||||
|
color: var(--t2);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-field input {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-check-grid,
|
||||||
|
.cactbot-role-grid,
|
||||||
|
.cactbot-player-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-role-grid,
|
||||||
|
.cactbot-player-columns {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-player-columns {
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-player-column {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-player-column-title {
|
||||||
|
padding: 0 2px 2px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-check-card {
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-check-card--role {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 18px 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cactbot-modal-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 18px 28px 24px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--bgcard);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.cactbot-export-options,
|
||||||
|
.cactbot-role-grid,
|
||||||
|
.cactbot-player-columns {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle-btn {
|
.view-toggle-btn {
|
||||||
|
|||||||
406
js/planner.js
406
js/planner.js
@ -165,6 +165,16 @@ function abilityEffectTooltip(ability, buffType, plan = null) {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function timelineAssignmentTooltip(assignment, mechanic, start, durationSec, cooldownSec, plan = null) {
|
||||||
|
return [
|
||||||
|
abilityEffectTooltip(assignment.ability, assignment.buffType, plan),
|
||||||
|
assignment.job ? `Caster: ${assignment.job}` : 'Caster: not assigned',
|
||||||
|
`Active: ${durationSec}s (${fmtTimestamp(start)} - ${fmtTimestamp(start + durationSec * 1000)})`,
|
||||||
|
`Cooldown: ${cooldownSec}s`,
|
||||||
|
mechanic?.name ? `Mechanic: ${mechanic.name} (${fmtTimestamp(mechanic.timestamp)})` : null,
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureActionMetaLoaded() {
|
async function ensureActionMetaLoaded() {
|
||||||
if (actionMetaPromise) return actionMetaPromise;
|
if (actionMetaPromise) return actionMetaPromise;
|
||||||
actionMetaPromise = (async () => {
|
actionMetaPromise = (async () => {
|
||||||
@ -449,7 +459,7 @@ function renderPlanDetail(plan) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-hint">Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten</div>
|
<div class="timeline-hint">Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Modifier+Scroll zum Zoomen · Klick für Zeiten</div>
|
||||||
<div id="planner-timeline">
|
<div id="planner-timeline">
|
||||||
${renderTimelineHtml(plan)}
|
${renderTimelineHtml(plan)}
|
||||||
</div>
|
</div>
|
||||||
@ -466,10 +476,6 @@ function renderPlanDetail(plan) {
|
|||||||
<button class="view-toggle-btn active" data-view="mechanics">Mechaniken</button>
|
<button class="view-toggle-btn active" data-view="mechanics">Mechaniken</button>
|
||||||
<button class="view-toggle-btn" data-view="myspells">★ Meine Spells</button>
|
<button class="view-toggle-btn" data-view="myspells">★ Meine Spells</button>
|
||||||
</div>
|
</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>
|
<button id="cactbot-export-btn" class="btn btn-sm" title="Cactbot Timeline exportieren">Cactbot Export</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -580,7 +586,7 @@ function cactbotExportFilename(plan) {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '') || 'mitigation-plan';
|
.replace(/^-+|-+$/g, '') || 'mitigation-plan';
|
||||||
return `${base}-mitigations.txt`;
|
return `${base}-mitigations`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cactbotAssignmentLabel(assignment, plan) {
|
function cactbotAssignmentLabel(assignment, plan) {
|
||||||
@ -588,16 +594,63 @@ function cactbotAssignmentLabel(assignment, plan) {
|
|||||||
return assignment.job ? `${assignment.job} ${ability}` : ability;
|
return assignment.job ? `${assignment.job} ${ability}` : ability;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cactbotFilterHints(plan) {
|
function cactbotJsString(text) {
|
||||||
const jobs = [...new Set((plan.jobComposition ?? []).filter(Boolean))].sort();
|
return String(text ?? '')
|
||||||
if (!jobs.length) return [];
|
.replace(/\\/g, '\\\\')
|
||||||
return ['# Optional job filters - uncomment lines you do not want to see:', ...jobs.map(job => `# hideall ".*${job} .*"`)];
|
.replace(/'/g, "\\'")
|
||||||
|
.replace(/\r?\n/g, '\\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function cactbotTimelineText(plan, splitEntries = false) {
|
function cactbotTemplateLiteral(text) {
|
||||||
|
return String(text ?? '')
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/`/g, '\\`')
|
||||||
|
.replace(/\$\{/g, '\\${');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotRegexText(text) {
|
||||||
|
return String(text ?? '')
|
||||||
|
.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&')
|
||||||
|
.replace(/'/g, "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotExportSlots(plan) {
|
||||||
|
return (plan.jobComposition ?? [])
|
||||||
|
.map((job, idx) => ({
|
||||||
|
idx,
|
||||||
|
job,
|
||||||
|
role: JOB_ROLE[job] ?? '',
|
||||||
|
name: plan.playerRoster?.[idx]?.name ?? '',
|
||||||
|
}))
|
||||||
|
.filter(slot => slot.job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotRoleLabel(role) {
|
||||||
|
return { healer: 'Healer', dps: 'DPS', tank: 'Tank' }[role] ?? role;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotRoleOrder(role) {
|
||||||
|
return { healer: 0, dps: 1, tank: 2 }[role] ?? 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotSelectedJobs(plan, settings) {
|
||||||
|
const selectedPlayers = new Set((settings.players ?? []).map(Number));
|
||||||
|
const slots = cactbotExportSlots(plan);
|
||||||
|
return new Set(slots
|
||||||
|
.filter(slot => selectedPlayers.has(slot.idx))
|
||||||
|
.map(slot => slot.job));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotAssignmentIncluded(assignment, selectedJobs) {
|
||||||
|
return !assignment.job || selectedJobs.has(assignment.job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotTimelineRows(plan, settings) {
|
||||||
|
const selectedJobs = cactbotSelectedJobs(plan, settings);
|
||||||
const byStart = new Map();
|
const byStart = new Map();
|
||||||
for (const mechanic of visiblePlanMechanics(plan)) {
|
for (const mechanic of visiblePlanMechanics(plan)) {
|
||||||
const planned = sortedAssignments(plannedAssignmentsForMechanic(plan, mechanic));
|
const planned = sortedAssignments(plannedAssignmentsForMechanic(plan, mechanic))
|
||||||
|
.filter(assignment => cactbotAssignmentIncluded(assignment, selectedJobs));
|
||||||
if (!planned.length) continue;
|
if (!planned.length) continue;
|
||||||
|
|
||||||
planned.forEach(assignment => {
|
planned.forEach(assignment => {
|
||||||
@ -613,19 +666,23 @@ function cactbotTimelineText(plan, splitEntries = false) {
|
|||||||
const rows = [];
|
const rows = [];
|
||||||
for (const [time, group] of byStart.entries()) {
|
for (const [time, group] of byStart.entries()) {
|
||||||
const assignments = [...group.assignments.values()];
|
const assignments = [...group.assignments.values()];
|
||||||
if (splitEntries) {
|
if (settings.splitEntries) {
|
||||||
assignments.forEach(text => rows.push({ time, text }));
|
assignments.forEach(text => rows.push({ time, text }));
|
||||||
} else {
|
} else {
|
||||||
rows.push({ time, text: assignments.join(', ') });
|
rows.push({ time, text: assignments.join(', ') });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.sort((a, b) => a.time - b.time || a.text.localeCompare(b.text));
|
rows.sort((a, b) => a.time - b.time || a.text.localeCompare(b.text));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotTimelineText(plan, settings) {
|
||||||
|
const rows = cactbotTimelineRows(plan, settings);
|
||||||
const header = [
|
const header = [
|
||||||
`# Exported from FF14 Mitigator`,
|
`# Exported from FF14 Mitigator`,
|
||||||
`# Plan: ${plan.name}`,
|
`# Plan: ${plan.name}`,
|
||||||
plan.source?.fightName ? `# Fight: ${plan.source.fightName}` : null,
|
plan.source?.fightName ? `# Fight: ${plan.source.fightName}` : null,
|
||||||
`# Format: cactbot timeline entries`,
|
`# Format: cactbot timeline entries`,
|
||||||
...cactbotFilterHints(plan),
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -634,6 +691,40 @@ function cactbotTimelineText(plan, splitEntries = false) {
|
|||||||
].join('\n') + '\n';
|
].join('\n') + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cactbotUserConfigText(plan, settings) {
|
||||||
|
const rows = cactbotTimelineRows(plan, settings);
|
||||||
|
const timeline = rows.map(row => ` ${cactbotTime(row.time)} "${cactbotEscape(row.text)}"`).join('\n');
|
||||||
|
const triggerRows = [...new Map(rows.map(row => [row.text, row])).values()];
|
||||||
|
const triggers = triggerRows.map((row, idx) => {
|
||||||
|
const idBase = `${plan.source?.fightName || plan.name}_${row.text}`
|
||||||
|
.replace(/[^A-Za-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.slice(0, 80) || `Mitigation_${idx + 1}`;
|
||||||
|
return ` {
|
||||||
|
id: '${cactbotJsString(idBase)}',
|
||||||
|
regex: /^${cactbotRegexText(row.text)}$/,
|
||||||
|
beforeSeconds: 1,
|
||||||
|
infoText: {
|
||||||
|
en: '${cactbotJsString(row.text)}',
|
||||||
|
},
|
||||||
|
}`;
|
||||||
|
}).join(',\n');
|
||||||
|
|
||||||
|
return `Options.Triggers.push(
|
||||||
|
{
|
||||||
|
zoneId: ${settings.zoneId || 'ZoneId.MatchAll'},
|
||||||
|
timeline: \`
|
||||||
|
${cactbotTemplateLiteral(timeline)}
|
||||||
|
\`,
|
||||||
|
timelineTriggers: [
|
||||||
|
${triggers}
|
||||||
|
],
|
||||||
|
triggers: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function downloadTextFile(filename, text) {
|
function downloadTextFile(filename, text) {
|
||||||
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@ -646,17 +737,150 @@ function downloadTextFile(filename, text) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initCactbotExport(planId) {
|
function cactbotExportModalHtml(plan) {
|
||||||
document.getElementById('cactbot-export-btn')?.addEventListener('click', () => {
|
const slots = cactbotExportSlots(plan);
|
||||||
const plan = getPlan(planId);
|
const roles = [...new Set(slots.map(slot => slot.role).filter(Boolean))]
|
||||||
if (!plan) return;
|
.sort((a, b) => cactbotRoleOrder(a) - cactbotRoleOrder(b));
|
||||||
const splitEntries = !!document.getElementById('cactbot-export-split')?.checked;
|
return `
|
||||||
const text = cactbotTimelineText(plan, splitEntries);
|
<div id="cactbot-export-modal" class="modal-overlay" style="display:flex">
|
||||||
if (!text.split('\n').some(line => /^\d/.test(line))) {
|
<div class="modal-box cactbot-export-modal-box">
|
||||||
|
<div class="cactbot-modal-header">
|
||||||
|
<div>
|
||||||
|
<div class="modal-title">Cactbot Export</div>
|
||||||
|
<div class="cactbot-modal-subtitle">${escHtml(plan.name)}</div>
|
||||||
|
</div>
|
||||||
|
<button id="cactbot-export-cancel" class="plan-btn" type="button" title="Schließen">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cactbot-export-options">
|
||||||
|
<label class="cactbot-option-card">
|
||||||
|
<input type="checkbox" id="cactbot-just-timeline">
|
||||||
|
<span>
|
||||||
|
<strong>Just timeline</strong>
|
||||||
|
<small>Nur .txt Einträge statt UserConfig .js</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="cactbot-option-card">
|
||||||
|
<input type="checkbox" id="cactbot-split-entries">
|
||||||
|
<span>
|
||||||
|
<strong>Separate entries</strong>
|
||||||
|
<small>Eine Timeline-Zeile pro Aktion</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cactbot-field">
|
||||||
|
<label for="cactbot-zone-id">ZoneId</label>
|
||||||
|
<input id="cactbot-zone-id" type="text" value="${escHtml(localStorage.getItem('ff14-cactbot-zone-id') || 'ZoneId.MatchAll')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cactbot-section">
|
||||||
|
<div class="cactbot-section-title">Roles</div>
|
||||||
|
<div class="cactbot-role-grid">
|
||||||
|
${roles.map(role => `
|
||||||
|
<label class="cactbot-check-card cactbot-check-card--role">
|
||||||
|
<input type="checkbox" class="cactbot-role-check" value="${escHtml(role)}" data-role="${escHtml(role)}" checked>
|
||||||
|
<span>${escHtml(cactbotRoleLabel(role))}</span>
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cactbot-section">
|
||||||
|
<div class="cactbot-section-title">Players</div>
|
||||||
|
<div class="cactbot-player-columns">
|
||||||
|
${roles.map(role => `
|
||||||
|
<div class="cactbot-player-column" data-role-column="${escHtml(role)}">
|
||||||
|
<div class="cactbot-player-column-title">${escHtml(cactbotRoleLabel(role))}</div>
|
||||||
|
${slots.filter(slot => slot.role === role).map(slot => `
|
||||||
|
<label class="cactbot-check-card">
|
||||||
|
<input type="checkbox" class="cactbot-player-check" value="${escHtml(slot.idx)}" data-role="${escHtml(role)}" checked>
|
||||||
|
<span>
|
||||||
|
<strong>${escHtml(slot.job)}</strong>
|
||||||
|
${slot.name ? `<small>${escHtml(slot.name)}</small>` : '<small>Slot ohne Namen</small>'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions cactbot-modal-actions">
|
||||||
|
<button id="cactbot-export-confirm" class="btn btn-gold">Export</button>
|
||||||
|
<button id="cactbot-export-cancel-footer" class="btn">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCactbotExportModal() {
|
||||||
|
document.getElementById('cactbot-export-modal')?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cactbotExportSettingsFromModal() {
|
||||||
|
const zoneId = document.getElementById('cactbot-zone-id')?.value.trim() || 'ZoneId.MatchAll';
|
||||||
|
localStorage.setItem('ff14-cactbot-zone-id', zoneId);
|
||||||
|
return {
|
||||||
|
justTimeline: !!document.getElementById('cactbot-just-timeline')?.checked,
|
||||||
|
splitEntries: !!document.getElementById('cactbot-split-entries')?.checked,
|
||||||
|
zoneId,
|
||||||
|
roles: [...document.querySelectorAll('.cactbot-role-check:checked')].map(input => input.value),
|
||||||
|
players: [...document.querySelectorAll('.cactbot-player-check:checked')].map(input => input.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCactbotExportModal(planId) {
|
||||||
|
closeCactbotExportModal();
|
||||||
|
const plan = getPlan(planId);
|
||||||
|
if (!plan) return;
|
||||||
|
document.body.insertAdjacentHTML('beforeend', cactbotExportModalHtml(plan));
|
||||||
|
const modal = document.getElementById('cactbot-export-modal');
|
||||||
|
document.getElementById('cactbot-export-cancel')?.addEventListener('click', closeCactbotExportModal);
|
||||||
|
document.getElementById('cactbot-export-cancel-footer')?.addEventListener('click', closeCactbotExportModal);
|
||||||
|
modal?.addEventListener('click', e => { if (e.target === modal) closeCactbotExportModal(); });
|
||||||
|
|
||||||
|
const playersForRole = role => [...modal.querySelectorAll('.cactbot-player-check')]
|
||||||
|
.filter(player => player.dataset.role === role);
|
||||||
|
const roleInputFor = role => [...modal.querySelectorAll('.cactbot-role-check')]
|
||||||
|
.find(input => input.dataset.role === role);
|
||||||
|
const syncRoleState = role => {
|
||||||
|
const players = playersForRole(role);
|
||||||
|
const roleInput = roleInputFor(role);
|
||||||
|
if (!roleInput || !players.length) return;
|
||||||
|
const checkedCount = players.filter(player => player.checked).length;
|
||||||
|
roleInput.checked = checkedCount === players.length;
|
||||||
|
roleInput.indeterminate = checkedCount > 0 && checkedCount < players.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
modal?.querySelectorAll('.cactbot-role-check').forEach(input => {
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
playersForRole(input.dataset.role).forEach(player => { player.checked = input.checked; });
|
||||||
|
input.indeterminate = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
modal?.querySelectorAll('.cactbot-player-check').forEach(input => {
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
syncRoleState(input.dataset.role);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
modal?.querySelectorAll('.cactbot-role-check').forEach(input => syncRoleState(input.dataset.role));
|
||||||
|
|
||||||
|
document.getElementById('cactbot-export-confirm')?.addEventListener('click', () => {
|
||||||
|
const settings = cactbotExportSettingsFromModal();
|
||||||
|
const text = settings.justTimeline ? cactbotTimelineText(plan, settings) : cactbotUserConfigText(plan, settings);
|
||||||
|
if (!text.split('\n').some(line => /^\s*\d/.test(line))) {
|
||||||
alert('Keine Mitigations zum Exportieren gefunden.');
|
alert('Keine Mitigations zum Exportieren gefunden.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadTextFile(cactbotExportFilename(plan), text);
|
downloadTextFile(`${cactbotExportFilename(plan)}.${settings.justTimeline ? 'txt' : 'js'}`, text);
|
||||||
|
closeCactbotExportModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCactbotExport(planId) {
|
||||||
|
document.getElementById('cactbot-export-btn')?.addEventListener('click', () => {
|
||||||
|
showCactbotExportModal(planId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,21 +990,32 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) {
|
|||||||
const targetTime = Number(targetMechanic.timestamp);
|
const targetTime = Number(targetMechanic.timestamp);
|
||||||
const tolerance = 50;
|
const tolerance = 50;
|
||||||
|
|
||||||
for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) {
|
const rows = timelinePlayerRows(plan);
|
||||||
if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) continue;
|
const seen = new Set();
|
||||||
|
const includeActiveAssignment = entry => {
|
||||||
|
if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) return;
|
||||||
|
|
||||||
// Persönliche Mitigation zeigt nur auf der Mechanik, der sie direkt zugewiesen ist
|
const key = `${entry.assignment.ability}::${entry.assignment.job || '*'}::${entry.start}`;
|
||||||
// (verhindert dass DRK-Cooldowns auf GNB-Tankbuster erscheinen und umgekehrt)
|
if (seen.has(key)) return;
|
||||||
if (TIMELINE_PERSONAL_ABILITIES.has(entry.assignment.ability)
|
seen.add(key);
|
||||||
&& entry.mechanic.id !== targetMechanic.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
...entry.assignment,
|
...entry.assignment,
|
||||||
sourceMechanicId: entry.mechanic.id,
|
sourceMechanicId: entry.mechanic.id,
|
||||||
sourceStart: entry.start,
|
sourceStart: entry.start,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
for (const entry of canonicalAssignmentActivations(plan, {
|
||||||
|
dedupeKey: item => canonicalTimelineKey(item, row),
|
||||||
|
includeEntry: item => {
|
||||||
|
const assignment = item.assignment;
|
||||||
|
if (assignment.ability !== row.ability) return false;
|
||||||
|
const assignedJob = assignment.job ?? '';
|
||||||
|
return !assignedJob || assignedJob === row.job;
|
||||||
|
},
|
||||||
|
})) includeActiveAssignment(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -936,6 +1171,11 @@ function initTimelineOptions(planId) {
|
|||||||
|
|
||||||
// ── Timeline ─────────────────────────────────────────────────────────────────
|
// ── Timeline ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function timelineZoom(plan) {
|
||||||
|
const zoom = Number(plan?.timelineZoom ?? 1);
|
||||||
|
return Math.max(0.5, Math.min(4, Number.isFinite(zoom) ? zoom : 1));
|
||||||
|
}
|
||||||
|
|
||||||
function planDurationMs(plan) {
|
function planDurationMs(plan) {
|
||||||
const sourceDuration = (plan.source?.fightEnd ?? 0) - (plan.source?.fightStart ?? 0);
|
const sourceDuration = (plan.source?.fightEnd ?? 0) - (plan.source?.fightStart ?? 0);
|
||||||
const mechanicEnd = Math.max(0, ...visiblePlanMechanics(plan).map(m => (m.timestamp ?? 0) + 30000));
|
const mechanicEnd = Math.max(0, ...visiblePlanMechanics(plan).map(m => (m.timestamp ?? 0) + 30000));
|
||||||
@ -944,7 +1184,7 @@ function planDurationMs(plan) {
|
|||||||
|
|
||||||
function timelineScale(plan) {
|
function timelineScale(plan) {
|
||||||
const duration = planDurationMs(plan);
|
const duration = planDurationMs(plan);
|
||||||
const pxPerSecond = 8;
|
const pxPerSecond = 8 * timelineZoom(plan);
|
||||||
return { duration, width: Math.max(720, Math.ceil(duration / 1000 * pxPerSecond)) };
|
return { duration, width: Math.max(720, Math.ceil(duration / 1000 * pxPerSecond)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1369,7 +1609,7 @@ function renderTimelineHtml(plan) {
|
|||||||
|
|
||||||
const playerRows = rows.map(row => {
|
const playerRows = rows.map(row => {
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
for (const entry of canonicalAssignmentActivations(plan, {
|
const rowEntries = canonicalAssignmentActivations(plan, {
|
||||||
dedupeKey: item => canonicalTimelineKey(item, row),
|
dedupeKey: item => canonicalTimelineKey(item, row),
|
||||||
includeEntry: item => {
|
includeEntry: item => {
|
||||||
const assignment = item.assignment;
|
const assignment = item.assignment;
|
||||||
@ -1377,7 +1617,41 @@ function renderTimelineHtml(plan) {
|
|||||||
const assignedJob = assignment.job ?? '';
|
const assignedJob = assignment.job ?? '';
|
||||||
return !assignedJob || assignedJob === row.job;
|
return !assignedJob || assignedJob === row.job;
|
||||||
},
|
},
|
||||||
})) {
|
}).sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
|
const gaps = [];
|
||||||
|
for (let i = 1; i < rowEntries.length; i++) {
|
||||||
|
const prev = rowEntries[i - 1];
|
||||||
|
const next = rowEntries[i];
|
||||||
|
const gapStart = Math.max(
|
||||||
|
prev.start + prev.durationSec * 1000,
|
||||||
|
prev.start + assignmentCooldownSeconds(prev.assignment) * 1000
|
||||||
|
);
|
||||||
|
const gapEnd = next.start;
|
||||||
|
if (gapEnd <= gapStart) continue;
|
||||||
|
const left = Math.max(0, Math.min(100, (gapStart / duration) * 100));
|
||||||
|
const widthPct = Math.max(0.2, Math.min(100 - left, ((gapEnd - gapStart) / duration) * 100));
|
||||||
|
const gapSeconds = cactbotTime(gapEnd - gapStart);
|
||||||
|
const requiredWindowMs = Math.max(
|
||||||
|
assignmentDurationSeconds(prev.assignment),
|
||||||
|
assignmentCooldownSeconds(prev.assignment)
|
||||||
|
) * 1000;
|
||||||
|
const latestExtraStart = gapEnd - requiredWindowMs;
|
||||||
|
const fitPct = Math.max(0, Math.min(100, ((latestExtraStart - gapStart) / (gapEnd - gapStart)) * 100));
|
||||||
|
const lateText = fitPct < 100
|
||||||
|
? `\nZu spät für einen weiteren Einsatz ab ${fmtTimestamp(Math.max(gapStart, latestExtraStart))}`
|
||||||
|
: '';
|
||||||
|
gaps.push(`
|
||||||
|
<span class="timeline-inactive-gap"
|
||||||
|
style="left:${left}%;width:${widthPct}%"
|
||||||
|
title="${escHtml(`${localizedAbilityName(row.ability, plan)} nicht aktiv: ${gapSeconds}s (${fmtTimestamp(gapStart)} - ${fmtTimestamp(gapEnd)})${lateText}`)}">
|
||||||
|
<span class="timeline-inactive-gap-fit" style="width:${fitPct}%"></span>
|
||||||
|
<span class="timeline-inactive-gap-late" style="left:${fitPct}%;width:${100 - fitPct}%"></span>
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of rowEntries) {
|
||||||
const item = entry;
|
const item = entry;
|
||||||
const m = item.mechanic;
|
const m = item.mechanic;
|
||||||
const a = item.assignment;
|
const a = item.assignment;
|
||||||
@ -1395,6 +1669,7 @@ function renderTimelineHtml(plan) {
|
|||||||
: 'timeline-mitigation--buff';
|
: 'timeline-mitigation--buff';
|
||||||
const icon = abilityIcon(a.ability);
|
const icon = abilityIcon(a.ability);
|
||||||
const abilityLabel = assignmentAbilityName(a, plan);
|
const abilityLabel = assignmentAbilityName(a, plan);
|
||||||
|
const tooltip = timelineAssignmentTooltip(a, m, start, durationSec, cooldownSec, plan);
|
||||||
blocks.push(`
|
blocks.push(`
|
||||||
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
|
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@ -1402,7 +1677,7 @@ function renderTimelineHtml(plan) {
|
|||||||
data-mechanic-id="${escHtml(m.id)}"
|
data-mechanic-id="${escHtml(m.id)}"
|
||||||
data-ability="${escHtml(a.ability)}"
|
data-ability="${escHtml(a.ability)}"
|
||||||
data-job="${escHtml(a.job ?? '')}"
|
data-job="${escHtml(a.job ?? '')}"
|
||||||
title="${escHtml(abilityLabel)} · aktiv ${durationSec}s · CD ${cooldownSec}s${a.job ? '' : ' · mögliche Zuordnung'}">
|
title="${escHtml(tooltip)}">
|
||||||
<span class="timeline-mitigation-active"></span>
|
<span class="timeline-mitigation-active"></span>
|
||||||
${icon ? `<img src="${escHtml(icon)}" alt="">` : ''}
|
${icon ? `<img src="${escHtml(icon)}" alt="">` : ''}
|
||||||
<span>${escHtml(abilityLabel)}</span>
|
<span>${escHtml(abilityLabel)}</span>
|
||||||
@ -1422,7 +1697,7 @@ function renderTimelineHtml(plan) {
|
|||||||
<span class="timeline-row-ability-name">${escHtml(abilityDisplayName)}</span>
|
<span class="timeline-row-ability-name">${escHtml(abilityDisplayName)}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-track" style="width:${width}px">${hitLines}${blocks.join('')}</div>
|
<div class="timeline-track" style="width:${width}px">${hitLines}${gaps.join('')}${blocks.join('')}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
@ -1467,7 +1742,7 @@ function renderTimelineSettingsHtml(plan) {
|
|||||||
<div class="timeline-settings-meta">${escHtml(mechanic.name)} bei ${escHtml(fmtTimestamp(mechanic.timestamp))}</div>
|
<div class="timeline-settings-meta">${escHtml(mechanic.name)} bei ${escHtml(fmtTimestamp(mechanic.timestamp))}</div>
|
||||||
<label>
|
<label>
|
||||||
<span>Start</span>
|
<span>Start</span>
|
||||||
<input class="timeline-setting-input" data-field="timestamp" type="number" min="0" step="1" value="${Math.round(((assignment.timestamp ?? mechanic.timestamp) || 0) / 1000)}">
|
<input class="timeline-setting-input" data-field="timestamp" type="number" min="0" step="0.1" value="${(((assignment.timestamp ?? mechanic.timestamp) || 0) / 1000).toFixed(1).replace(/\.0$/, '')}">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Dauer</span>
|
<span>Dauer</span>
|
||||||
@ -1573,7 +1848,12 @@ function showTimelineMenu(x, y, items) {
|
|||||||
menu.id = 'timeline-context-menu';
|
menu.id = 'timeline-context-menu';
|
||||||
menu.className = 'timeline-context-menu';
|
menu.className = 'timeline-context-menu';
|
||||||
menu.innerHTML = items.length
|
menu.innerHTML = items.length
|
||||||
? items.map((item, idx) => `
|
? items.map((item, idx) => item.type === 'timeInput' ? `
|
||||||
|
<label class="timeline-menu-time">
|
||||||
|
<span>${escHtml(item.label)}</span>
|
||||||
|
<input type="number" min="0" step="0.1" value="${escHtml(item.value)}" data-idx="${idx}">
|
||||||
|
</label>
|
||||||
|
` : `
|
||||||
<button class="timeline-menu-item${item.disabled ? ' disabled' : ''}${item.header ? ' timeline-menu-header' : ''}" data-idx="${idx}"${item.disabled ? ' disabled' : ''}>
|
<button class="timeline-menu-item${item.disabled ? ' disabled' : ''}${item.header ? ' timeline-menu-header' : ''}" data-idx="${idx}"${item.disabled ? ' disabled' : ''}>
|
||||||
${item.icon ? `<img src="${escHtml(item.icon)}" alt="">` : ''}
|
${item.icon ? `<img src="${escHtml(item.icon)}" alt="">` : ''}
|
||||||
<span>${escHtml(item.label)}</span>
|
<span>${escHtml(item.label)}</span>
|
||||||
@ -1594,6 +1874,13 @@ function showTimelineMenu(x, y, items) {
|
|||||||
if (!item?.keepOpen) closeTimelineMenu();
|
if (!item?.keepOpen) closeTimelineMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
menu.addEventListener('change', e => {
|
||||||
|
const input = e.target.closest('.timeline-menu-time input');
|
||||||
|
if (!input) return;
|
||||||
|
const item = items[parseInt(input.dataset.idx, 10)];
|
||||||
|
item?.onChange?.(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
const closeOutside = ev => {
|
const closeOutside = ev => {
|
||||||
if (menu.contains(ev.target)) return;
|
if (menu.contains(ev.target)) return;
|
||||||
closeTimelineMenu();
|
closeTimelineMenu();
|
||||||
@ -1932,6 +2219,35 @@ function initTimeline(planId) {
|
|||||||
timelinePan = null;
|
timelinePan = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
timeline.addEventListener('wheel', e => {
|
||||||
|
if (!(e.ctrlKey || e.metaKey || e.altKey)) return;
|
||||||
|
const scroll = e.target.closest('.timeline-scroll');
|
||||||
|
if (!scroll) return;
|
||||||
|
const plan = getPlan(planId);
|
||||||
|
if (!plan) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const oldTrack = scroll.querySelector('.timeline-track');
|
||||||
|
const oldWidth = oldTrack?.getBoundingClientRect().width || 1;
|
||||||
|
const rect = scroll.getBoundingClientRect();
|
||||||
|
const cursorX = e.clientX - rect.left;
|
||||||
|
const contentX = scroll.scrollLeft + cursorX;
|
||||||
|
const ratio = contentX / oldWidth;
|
||||||
|
const currentZoom = timelineZoom(plan);
|
||||||
|
const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||||
|
const nextZoom = Math.max(0.5, Math.min(4, currentZoom * factor));
|
||||||
|
if (Math.abs(nextZoom - currentZoom) < 0.001) return;
|
||||||
|
|
||||||
|
updatePlan(planId, { timelineZoom: nextZoom });
|
||||||
|
refreshTimeline(planId);
|
||||||
|
const nextScroll = timeline.querySelector('.timeline-scroll');
|
||||||
|
const nextTrack = nextScroll?.querySelector('.timeline-track');
|
||||||
|
const nextWidth = nextTrack?.getBoundingClientRect().width || oldWidth;
|
||||||
|
if (nextScroll) {
|
||||||
|
nextScroll.scrollLeft = Math.max(0, ratio * nextWidth - cursorX);
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
timeline.addEventListener('click', e => {
|
timeline.addEventListener('click', e => {
|
||||||
if (suppressNextTimelineClick) {
|
if (suppressNextTimelineClick) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -1983,7 +2299,19 @@ function initTimeline(planId) {
|
|||||||
const rows = timelinePlayerRows(plan ?? {});
|
const rows = timelinePlayerRows(plan ?? {});
|
||||||
const found = findTimelineAssignment(plan, selectedTimelineAssignment);
|
const found = findTimelineAssignment(plan, selectedTimelineAssignment);
|
||||||
const timestamp = found ? assignmentStartMs(found.mechanic, found.assignment) : 0;
|
const timestamp = found ? assignmentStartMs(found.mechanic, found.assignment) : 0;
|
||||||
const items = rows
|
const items = [{
|
||||||
|
type: 'timeInput',
|
||||||
|
label: 'Start (s)',
|
||||||
|
value: (timestamp / 1000).toFixed(1).replace(/\.0$/, ''),
|
||||||
|
onChange: value => setTimelineAssignmentField(
|
||||||
|
planId,
|
||||||
|
block.dataset.mechanicId,
|
||||||
|
block.dataset.ability,
|
||||||
|
block.dataset.job,
|
||||||
|
'timestamp',
|
||||||
|
value
|
||||||
|
),
|
||||||
|
}, ...rows
|
||||||
.filter(row => jobCanUseAbility(row.job, block.dataset.ability))
|
.filter(row => jobCanUseAbility(row.job, block.dataset.ability))
|
||||||
.filter((row, idx, arr) => arr.findIndex(r => r.job === row.job) === idx)
|
.filter((row, idx, arr) => arr.findIndex(r => r.job === row.job) === idx)
|
||||||
.map(row => ({
|
.map(row => ({
|
||||||
@ -1995,7 +2323,7 @@ function initTimeline(planId) {
|
|||||||
job: block.dataset.job,
|
job: block.dataset.job,
|
||||||
}, found?.assignment ?? null),
|
}, found?.assignment ?? null),
|
||||||
onClick: () => setTimelineAssignmentJob(planId, block.dataset.mechanicId, block.dataset.ability, block.dataset.job, row.job),
|
onClick: () => setTimelineAssignmentJob(planId, block.dataset.mechanicId, block.dataset.ability, block.dataset.job, row.job),
|
||||||
}));
|
}))];
|
||||||
showTimelineMenu(e.clientX, e.clientY, items);
|
showTimelineMenu(e.clientX, e.clientY, items);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user