forked from xziino/ff14-mitigator
planner improvmnets
This commit is contained in:
parent
e5c12b6915
commit
e78ecbd0d9
162
js/planner.js
162
js/planner.js
@ -2133,58 +2133,141 @@ function initTimeline(planId) {
|
|||||||
let timelinePan = null;
|
let timelinePan = null;
|
||||||
let suppressNextTimelineClick = false;
|
let suppressNextTimelineClick = false;
|
||||||
let timelineDrag = null;
|
let timelineDrag = null;
|
||||||
|
let dragPreviewEl = null;
|
||||||
|
let dragPreviewRow = null;
|
||||||
|
let dragPreviewTrack = null;
|
||||||
|
let pendingDragPreview = null;
|
||||||
|
let dragPreviewFrame = 0;
|
||||||
|
|
||||||
function removeDragPreview() {
|
function removeDragPreview() {
|
||||||
document.getElementById('timeline-drag-preview')?.remove();
|
if (dragPreviewFrame) cancelAnimationFrame(dragPreviewFrame);
|
||||||
timeline.querySelectorAll('.timeline-player-row--drop-ok, .timeline-player-row--drop-bad')
|
dragPreviewFrame = 0;
|
||||||
.forEach(row => row.classList.remove('timeline-player-row--drop-ok', 'timeline-player-row--drop-bad'));
|
pendingDragPreview = null;
|
||||||
|
dragPreviewEl?.remove();
|
||||||
|
dragPreviewEl = null;
|
||||||
|
dragPreviewTrack = null;
|
||||||
|
dragPreviewRow?.classList.remove('timeline-player-row--drop-ok', 'timeline-player-row--drop-bad');
|
||||||
|
dragPreviewRow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDragPreview(event) {
|
function setDragPreviewRow(row, valid) {
|
||||||
if (!timelineDrag) return;
|
if (dragPreviewRow && dragPreviewRow !== row) {
|
||||||
removeDragPreview();
|
dragPreviewRow.classList.remove('timeline-player-row--drop-ok', 'timeline-player-row--drop-bad');
|
||||||
|
}
|
||||||
|
dragPreviewRow = row;
|
||||||
|
row.classList.toggle('timeline-player-row--drop-ok', valid);
|
||||||
|
row.classList.toggle('timeline-player-row--drop-bad', !valid);
|
||||||
|
}
|
||||||
|
|
||||||
const track = event.target.closest('.timeline-player-row .timeline-track');
|
function ensureDragPreview(track) {
|
||||||
const row = event.target.closest('.timeline-player-row');
|
if (!dragPreviewEl) {
|
||||||
|
dragPreviewEl = document.createElement('div');
|
||||||
|
dragPreviewEl.id = 'timeline-drag-preview';
|
||||||
|
dragPreviewEl.innerHTML = `
|
||||||
|
<span class="timeline-drag-preview-active"></span>
|
||||||
|
${timelineDrag.icon ? `<img src="${escHtml(timelineDrag.icon)}" alt="">` : ''}
|
||||||
|
<span>${escHtml(timelineDrag.label)}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (dragPreviewTrack !== track) {
|
||||||
|
track.appendChild(dragPreviewEl);
|
||||||
|
dragPreviewTrack = track;
|
||||||
|
}
|
||||||
|
return dragPreviewEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDragPreview(target, clientX) {
|
||||||
|
if (!timelineDrag) return;
|
||||||
|
|
||||||
|
const track = target.closest('.timeline-player-row .timeline-track');
|
||||||
|
const row = target.closest('.timeline-player-row');
|
||||||
if (!track || !row) return;
|
if (!track || !row) return;
|
||||||
|
|
||||||
const plan = getPlan(planId);
|
const plan = timelineDrag.plan ?? getPlan(planId);
|
||||||
if (!plan) return;
|
if (!plan) return;
|
||||||
const rect = track.getBoundingClientRect();
|
const rect = timelineDrag.track === track && timelineDrag.trackRect
|
||||||
const duration = planDurationMs(plan);
|
? timelineDrag.trackRect
|
||||||
const deltaPx = event.clientX - timelineDrag.startClientX;
|
: track.getBoundingClientRect();
|
||||||
|
timelineDrag.track = track;
|
||||||
|
timelineDrag.trackRect = rect;
|
||||||
|
const duration = timelineDrag.planDuration || planDurationMs(plan);
|
||||||
|
const deltaPx = clientX - timelineDrag.startClientX;
|
||||||
const timestamp = Math.max(0, timelineDrag.startTimestamp + (deltaPx / rect.width) * duration);
|
const timestamp = Math.max(0, timelineDrag.startTimestamp + (deltaPx / rect.width) * duration);
|
||||||
const left = Math.max(0, Math.min(100, (timestamp / duration) * 100));
|
const left = Math.max(0, Math.min(100, (timestamp / duration) * 100));
|
||||||
const durationPct = Math.max(1.2, Math.min(100 - left, (timelineDrag.durationSec * 1000 / duration) * 100));
|
const durationPct = Math.max(1.2, Math.min(100 - left, (timelineDrag.durationSec * 1000 / duration) * 100));
|
||||||
const cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100));
|
const cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100));
|
||||||
const activePct = Math.min(100, (durationPct / cooldownPct) * 100);
|
const activePct = Math.min(100, (durationPct / cooldownPct) * 100);
|
||||||
|
const rowJob = row.dataset.job;
|
||||||
const valid = row.dataset.ability === timelineDrag.ability
|
const valid = row.dataset.ability === timelineDrag.ability
|
||||||
&& jobCanUseAbility(row.dataset.job, timelineDrag.ability)
|
&& jobCanUseAbility(rowJob, timelineDrag.ability)
|
||||||
&& !assignmentOverlapsJob(plan, row.dataset.job, timelineDrag.ability, timestamp, {
|
&& !dragTimestampOverlapsJob(rowJob, timestamp);
|
||||||
mechanicId: timelineDrag.mechanicId,
|
|
||||||
ability: timelineDrag.ability,
|
|
||||||
job: timelineDrag.job,
|
|
||||||
}, {
|
|
||||||
ability: timelineDrag.ability,
|
|
||||||
job: row.dataset.job,
|
|
||||||
durationSeconds: timelineDrag.durationSec,
|
|
||||||
cooldownSeconds: timelineDrag.cooldownSec,
|
|
||||||
});
|
|
||||||
|
|
||||||
row.classList.add(valid ? 'timeline-player-row--drop-ok' : 'timeline-player-row--drop-bad');
|
setDragPreviewRow(row, valid);
|
||||||
|
|
||||||
const preview = document.createElement('div');
|
const preview = ensureDragPreview(track);
|
||||||
preview.id = 'timeline-drag-preview';
|
|
||||||
preview.className = `timeline-drag-preview ${valid ? '' : 'timeline-drag-preview--bad'}`;
|
preview.className = `timeline-drag-preview ${valid ? '' : 'timeline-drag-preview--bad'}`;
|
||||||
preview.style.left = `${left}%`;
|
preview.style.left = `${left}%`;
|
||||||
preview.style.setProperty('--cd-width', `${cooldownPct}%`);
|
preview.style.setProperty('--cd-width', `${cooldownPct}%`);
|
||||||
preview.style.setProperty('--active-width', `${activePct}%`);
|
preview.style.setProperty('--active-width', `${activePct}%`);
|
||||||
preview.innerHTML = `
|
}
|
||||||
<span class="timeline-drag-preview-active"></span>
|
|
||||||
${timelineDrag.icon ? `<img src="${escHtml(timelineDrag.icon)}" alt="">` : ''}
|
function scheduleDragPreview(event) {
|
||||||
<span>${escHtml(timelineDrag.label)}</span>
|
pendingDragPreview = {
|
||||||
`;
|
target: event.target,
|
||||||
track.appendChild(preview);
|
clientX: event.clientX,
|
||||||
|
};
|
||||||
|
if (dragPreviewFrame) return;
|
||||||
|
dragPreviewFrame = requestAnimationFrame(() => {
|
||||||
|
dragPreviewFrame = 0;
|
||||||
|
const next = pendingDragPreview;
|
||||||
|
pendingDragPreview = null;
|
||||||
|
if (next) updateDragPreview(next.target, next.clientX);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDragOverlapEntries(plan, ref) {
|
||||||
|
const ignoredActivation = assignmentEntryForRef(plan, ref);
|
||||||
|
const byJob = new Map();
|
||||||
|
const addEntry = (job, entry) => {
|
||||||
|
if (!byJob.has(job)) byJob.set(job, []);
|
||||||
|
byJob.get(job).push(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const mechanic of visiblePlanMechanics(plan)) {
|
||||||
|
for (const assignment of mechanic.assignments ?? []) {
|
||||||
|
if (assignment.ability !== ref.ability) continue;
|
||||||
|
if (sameAssignmentRef(mechanic, assignment, ref)) continue;
|
||||||
|
|
||||||
|
const start = assignmentStartMs(mechanic, assignment);
|
||||||
|
const entry = {
|
||||||
|
start,
|
||||||
|
end: start + assignmentWindowMs(assignment),
|
||||||
|
};
|
||||||
|
if (ignoredActivation) {
|
||||||
|
const fullEntry = { mechanic, assignment, ...entry };
|
||||||
|
if (assignmentsOverlapActiveFrame(plan, ignoredActivation, fullEntry)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedJob = assignment.job ?? '';
|
||||||
|
if (assignedJob) {
|
||||||
|
addEntry(assignedJob, entry);
|
||||||
|
} else {
|
||||||
|
(plan.jobComposition ?? [])
|
||||||
|
.filter(job => jobCanUseAbility(job, assignment.ability))
|
||||||
|
.forEach(job => addEntry(job, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return byJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragTimestampOverlapsJob(rowJob, timestamp) {
|
||||||
|
if (!timelineDrag) return false;
|
||||||
|
const candidateStart = Math.max(0, timestamp);
|
||||||
|
const candidateEnd = candidateStart + timelineDrag.windowMs;
|
||||||
|
const entries = timelineDrag.overlapEntriesByJob?.get(rowJob) ?? [];
|
||||||
|
return entries.some(entry => candidateStart < entry.end && candidateEnd > entry.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeline.addEventListener('pointerdown', e => {
|
timeline.addEventListener('pointerdown', e => {
|
||||||
@ -2386,6 +2469,8 @@ function initTimeline(planId) {
|
|||||||
transparent.height = 1;
|
transparent.height = 1;
|
||||||
e.dataTransfer.setDragImage(transparent, 0, 0);
|
e.dataTransfer.setDragImage(transparent, 0, 0);
|
||||||
timelineDrag = {
|
timelineDrag = {
|
||||||
|
plan,
|
||||||
|
planDuration: planDurationMs(plan),
|
||||||
mechanicId: block.dataset.mechanicId,
|
mechanicId: block.dataset.mechanicId,
|
||||||
ability: block.dataset.ability,
|
ability: block.dataset.ability,
|
||||||
job: block.dataset.job,
|
job: block.dataset.job,
|
||||||
@ -2395,6 +2480,14 @@ function initTimeline(planId) {
|
|||||||
startTimestamp: assignmentStartMs(found.mechanic, found.assignment),
|
startTimestamp: assignmentStartMs(found.mechanic, found.assignment),
|
||||||
durationSec: assignmentDurationSeconds(found.assignment),
|
durationSec: assignmentDurationSeconds(found.assignment),
|
||||||
cooldownSec: assignmentCooldownSeconds(found.assignment),
|
cooldownSec: assignmentCooldownSeconds(found.assignment),
|
||||||
|
windowMs: assignmentWindowMs(found.assignment),
|
||||||
|
overlapEntriesByJob: buildDragOverlapEntries(plan, {
|
||||||
|
mechanicId: block.dataset.mechanicId,
|
||||||
|
ability: block.dataset.ability,
|
||||||
|
job: block.dataset.job,
|
||||||
|
}),
|
||||||
|
track: null,
|
||||||
|
trackRect: null,
|
||||||
};
|
};
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', JSON.stringify({
|
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||||||
@ -2409,7 +2502,7 @@ function initTimeline(planId) {
|
|||||||
timeline.addEventListener('dragover', e => {
|
timeline.addEventListener('dragover', e => {
|
||||||
if (e.target.closest('.timeline-player-row .timeline-track')) {
|
if (e.target.closest('.timeline-player-row .timeline-track')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
updateDragPreview(e);
|
scheduleDragPreview(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2418,6 +2511,9 @@ function initTimeline(planId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
timeline.addEventListener('drop', e => {
|
timeline.addEventListener('drop', e => {
|
||||||
|
if (dragPreviewFrame) cancelAnimationFrame(dragPreviewFrame);
|
||||||
|
dragPreviewFrame = 0;
|
||||||
|
pendingDragPreview = null;
|
||||||
removeDragPreview();
|
removeDragPreview();
|
||||||
const track = e.target.closest('.timeline-player-row .timeline-track');
|
const track = e.target.closest('.timeline-player-row .timeline-track');
|
||||||
const row = e.target.closest('.timeline-player-row');
|
const row = e.target.closest('.timeline-player-row');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user