planner improvmnets

This commit is contained in:
Akurosia Kamo 2026-06-11 11:00:34 +02:00
parent e5c12b6915
commit e78ecbd0d9

View File

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