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 suppressNextTimelineClick = false;
|
||||
let timelineDrag = null;
|
||||
let dragPreviewEl = null;
|
||||
let dragPreviewRow = null;
|
||||
let dragPreviewTrack = null;
|
||||
let pendingDragPreview = null;
|
||||
let dragPreviewFrame = 0;
|
||||
|
||||
function removeDragPreview() {
|
||||
document.getElementById('timeline-drag-preview')?.remove();
|
||||
timeline.querySelectorAll('.timeline-player-row--drop-ok, .timeline-player-row--drop-bad')
|
||||
.forEach(row => row.classList.remove('timeline-player-row--drop-ok', 'timeline-player-row--drop-bad'));
|
||||
if (dragPreviewFrame) cancelAnimationFrame(dragPreviewFrame);
|
||||
dragPreviewFrame = 0;
|
||||
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) {
|
||||
if (!timelineDrag) return;
|
||||
removeDragPreview();
|
||||
function setDragPreviewRow(row, valid) {
|
||||
if (dragPreviewRow && dragPreviewRow !== row) {
|
||||
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');
|
||||
const row = event.target.closest('.timeline-player-row');
|
||||
function ensureDragPreview(track) {
|
||||
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;
|
||||
|
||||
const plan = getPlan(planId);
|
||||
const plan = timelineDrag.plan ?? getPlan(planId);
|
||||
if (!plan) return;
|
||||
const rect = track.getBoundingClientRect();
|
||||
const duration = planDurationMs(plan);
|
||||
const deltaPx = event.clientX - timelineDrag.startClientX;
|
||||
const rect = timelineDrag.track === track && timelineDrag.trackRect
|
||||
? timelineDrag.trackRect
|
||||
: 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 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 cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100));
|
||||
const activePct = Math.min(100, (durationPct / cooldownPct) * 100);
|
||||
const rowJob = row.dataset.job;
|
||||
const valid = row.dataset.ability === timelineDrag.ability
|
||||
&& jobCanUseAbility(row.dataset.job, timelineDrag.ability)
|
||||
&& !assignmentOverlapsJob(plan, row.dataset.job, timelineDrag.ability, timestamp, {
|
||||
mechanicId: timelineDrag.mechanicId,
|
||||
ability: timelineDrag.ability,
|
||||
job: timelineDrag.job,
|
||||
}, {
|
||||
ability: timelineDrag.ability,
|
||||
job: row.dataset.job,
|
||||
durationSeconds: timelineDrag.durationSec,
|
||||
cooldownSeconds: timelineDrag.cooldownSec,
|
||||
});
|
||||
&& jobCanUseAbility(rowJob, timelineDrag.ability)
|
||||
&& !dragTimestampOverlapsJob(rowJob, timestamp);
|
||||
|
||||
row.classList.add(valid ? 'timeline-player-row--drop-ok' : 'timeline-player-row--drop-bad');
|
||||
setDragPreviewRow(row, valid);
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.id = 'timeline-drag-preview';
|
||||
const preview = ensureDragPreview(track);
|
||||
preview.className = `timeline-drag-preview ${valid ? '' : 'timeline-drag-preview--bad'}`;
|
||||
preview.style.left = `${left}%`;
|
||||
preview.style.setProperty('--cd-width', `${cooldownPct}%`);
|
||||
preview.style.setProperty('--active-width', `${activePct}%`);
|
||||
preview.innerHTML = `
|
||||
<span class="timeline-drag-preview-active"></span>
|
||||
${timelineDrag.icon ? `<img src="${escHtml(timelineDrag.icon)}" alt="">` : ''}
|
||||
<span>${escHtml(timelineDrag.label)}</span>
|
||||
`;
|
||||
track.appendChild(preview);
|
||||
}
|
||||
|
||||
function scheduleDragPreview(event) {
|
||||
pendingDragPreview = {
|
||||
target: event.target,
|
||||
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 => {
|
||||
@ -2386,6 +2469,8 @@ function initTimeline(planId) {
|
||||
transparent.height = 1;
|
||||
e.dataTransfer.setDragImage(transparent, 0, 0);
|
||||
timelineDrag = {
|
||||
plan,
|
||||
planDuration: planDurationMs(plan),
|
||||
mechanicId: block.dataset.mechanicId,
|
||||
ability: block.dataset.ability,
|
||||
job: block.dataset.job,
|
||||
@ -2395,6 +2480,14 @@ function initTimeline(planId) {
|
||||
startTimestamp: assignmentStartMs(found.mechanic, found.assignment),
|
||||
durationSec: assignmentDurationSeconds(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.setData('text/plain', JSON.stringify({
|
||||
@ -2409,7 +2502,7 @@ function initTimeline(planId) {
|
||||
timeline.addEventListener('dragover', e => {
|
||||
if (e.target.closest('.timeline-player-row .timeline-track')) {
|
||||
e.preventDefault();
|
||||
updateDragPreview(e);
|
||||
scheduleDragPreview(e);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2418,6 +2511,9 @@ function initTimeline(planId) {
|
||||
});
|
||||
|
||||
timeline.addEventListener('drop', e => {
|
||||
if (dragPreviewFrame) cancelAnimationFrame(dragPreviewFrame);
|
||||
dragPreviewFrame = 0;
|
||||
pendingDragPreview = null;
|
||||
removeDragPreview();
|
||||
const track = e.target.closest('.timeline-player-row .timeline-track');
|
||||
const row = e.target.closest('.timeline-player-row');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user