forked from xziino/ff14-mitigator
Planer: Gantt-Zeilen pro Ability + aktive Dauer im Balken sichtbar
- Eine Timeline-Zeile pro Ability statt pro Job - Schilde ausgeblendet (außer Panhaima SGE) - Balken zeigt aktive Dauer vs. Cooldown via Gradient - Klick auf leere Zeile fügt Ability direkt hinzu - Drop nur auf passende Ability-Zeile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cdd594e43e
commit
da226d54a2
@ -805,6 +805,33 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline-row-ability {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-row-ability-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.timeline-row-ability-name {
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline-player-row--job-start .timeline-row-label,
|
||||
.timeline-player-row--job-start .timeline-track {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.timeline-boss-action {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@ -848,16 +875,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-mitigation-active {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--active-width);
|
||||
background: currentColor;
|
||||
opacity: 0.22;
|
||||
pointer-events: none;
|
||||
}
|
||||
.timeline-mitigation-active { display: none; }
|
||||
|
||||
.timeline-mitigation:active { cursor: grabbing; }
|
||||
.timeline-mitigation.selected {
|
||||
@ -891,9 +909,21 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.timeline-mitigation--buff { border-color: rgba(200,168,75,.5); color: var(--gold); background: rgba(200,168,75,.12); }
|
||||
.timeline-mitigation--debuff { border-color: rgba(224,92,92,.5); color: var(--red); background: rgba(224,92,92,.12); }
|
||||
.timeline-mitigation--shield { border-color: rgba(74,158,255,.5); color: var(--blue); background: rgba(74,158,255,.12); }
|
||||
.timeline-mitigation--buff {
|
||||
border-color: rgba(200,168,75,.5);
|
||||
color: var(--gold);
|
||||
background: linear-gradient(to right, rgba(200,168,75,.30) 0%, rgba(200,168,75,.30) var(--active-width), rgba(200,168,75,.06) var(--active-width), rgba(200,168,75,.06) 100%);
|
||||
}
|
||||
.timeline-mitigation--debuff {
|
||||
border-color: rgba(224,92,92,.5);
|
||||
color: var(--red);
|
||||
background: linear-gradient(to right, rgba(224,92,92,.30) 0%, rgba(224,92,92,.30) var(--active-width), rgba(224,92,92,.06) var(--active-width), rgba(224,92,92,.06) 100%);
|
||||
}
|
||||
.timeline-mitigation--shield {
|
||||
border-color: rgba(74,158,255,.5);
|
||||
color: var(--blue);
|
||||
background: linear-gradient(to right, rgba(74,158,255,.30) 0%, rgba(74,158,255,.30) var(--active-width), rgba(74,158,255,.06) var(--active-width), rgba(74,158,255,.06) 100%);
|
||||
}
|
||||
|
||||
.timeline-axis {
|
||||
min-height: 28px;
|
||||
|
||||
@ -582,12 +582,18 @@ function timelineScale(plan) {
|
||||
|
||||
function timelinePlayerRows(plan) {
|
||||
const roster = plan.playerRoster ?? [];
|
||||
return (plan.jobComposition ?? []).map((job, idx) => ({
|
||||
idx,
|
||||
job,
|
||||
name: roster[idx]?.name ?? '',
|
||||
role: JOB_ROLE[job] ?? '',
|
||||
})).filter(row => row.job);
|
||||
const rows = [];
|
||||
(plan.jobComposition ?? []).forEach((job, idx) => {
|
||||
if (!job) return;
|
||||
const name = roster[idx]?.name ?? '';
|
||||
const role = JOB_ROLE[job] ?? '';
|
||||
const abilities = (JOB_ABILITIES[job] ?? [])
|
||||
.filter(ab => ab.buffType !== 'shield' || ab.name === 'Panhaima');
|
||||
abilities.forEach((ab, abilityIdx) => {
|
||||
rows.push({ idx, job, ability: ab.name, buffType: ab.buffType, name, role, firstForJob: abilityIdx === 0 });
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function selectedAssignmentMatches(mechanicId, assignment) {
|
||||
@ -603,8 +609,11 @@ function jobCanUseAbility(job, ability) {
|
||||
|
||||
function timelineRowsForAssignment(rows, assignment) {
|
||||
const assignedJob = assignment.job ?? '';
|
||||
if (assignedJob) return rows.filter(row => row.job === assignedJob);
|
||||
return rows.filter(row => jobCanUseAbility(row.job, assignment.ability));
|
||||
return rows.filter(row => {
|
||||
if (row.ability !== assignment.ability) return false;
|
||||
if (assignedJob) return row.job === assignedJob;
|
||||
return true; // unresolved: show in all rows for this ability
|
||||
});
|
||||
}
|
||||
|
||||
function assignmentStartMs(mechanic, assignment) {
|
||||
@ -707,7 +716,9 @@ function renderTimelineHtml(plan) {
|
||||
const blocks = [];
|
||||
for (const m of mechanics) {
|
||||
for (const a of sortedAssignments(m.assignments ?? [])) {
|
||||
if (!timelineRowsForAssignment(rows, a).some(target => target.job === row.job)) continue;
|
||||
if (a.ability !== row.ability) continue;
|
||||
const assignedJob = a.job ?? '';
|
||||
if (assignedJob && assignedJob !== row.job) continue;
|
||||
const start = Number.isFinite(Number(a.timestamp)) ? Number(a.timestamp) : m.timestamp;
|
||||
const durationSec = assignmentDurationSeconds(a);
|
||||
const cooldownSec = assignmentCooldownSeconds(a);
|
||||
@ -721,7 +732,7 @@ function renderTimelineHtml(plan) {
|
||||
: a.buffType === 'shield' ? 'timeline-mitigation--shield'
|
||||
: 'timeline-mitigation--buff';
|
||||
const icon = MITIG_ICONS[a.ability] ?? '';
|
||||
const ability = assignmentAbilityName(a, plan);
|
||||
const abilityLabel = assignmentAbilityName(a, plan);
|
||||
blocks.push(`
|
||||
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
|
||||
draggable="true"
|
||||
@ -729,19 +740,25 @@ function renderTimelineHtml(plan) {
|
||||
data-mechanic-id="${escHtml(m.id)}"
|
||||
data-ability="${escHtml(a.ability)}"
|
||||
data-job="${escHtml(a.job ?? '')}"
|
||||
title="${escHtml(ability)} · aktiv ${durationSec}s · CD ${cooldownSec}s${a.job ? '' : ' · mögliche Zuordnung'}">
|
||||
title="${escHtml(abilityLabel)} · aktiv ${durationSec}s · CD ${cooldownSec}s${a.job ? '' : ' · mögliche Zuordnung'}">
|
||||
<span class="timeline-mitigation-active"></span>
|
||||
${icon ? `<img src="${escHtml(icon)}" alt="">` : ''}
|
||||
<span>${escHtml(ability)}</span>
|
||||
<span>${escHtml(abilityLabel)}</span>
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
}
|
||||
const icon = MITIG_ICONS[row.ability] ?? '';
|
||||
const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability;
|
||||
const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : '';
|
||||
return `
|
||||
<div class="timeline-row timeline-player-row" data-row-idx="${row.idx}" data-job="${escHtml(row.job)}">
|
||||
<div class="timeline-row timeline-player-row${jobStartCls}" data-row-idx="${row.idx}" data-job="${escHtml(row.job)}" data-ability="${escHtml(row.ability)}">
|
||||
<div class="timeline-row-label">
|
||||
<span class="timeline-job role-${escHtml(row.role)}">${escHtml(row.job)}</span>
|
||||
<span class="timeline-player-name">${escHtml(row.name || `Slot ${row.idx + 1}`)}</span>
|
||||
<span class="timeline-row-ability">
|
||||
${icon ? `<img src="${escHtml(icon)}" alt="" class="timeline-row-ability-icon">` : ''}
|
||||
<span class="timeline-row-ability-name">${escHtml(abilityDisplayName)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="timeline-track" style="width:${width}px">${hitLines}${blocks.join('')}</div>
|
||||
</div>`;
|
||||
@ -977,14 +994,11 @@ function initTimeline(planId) {
|
||||
const timestamp = timelineTimestampFromEvent(plan, track, e);
|
||||
const mechanic = findNearestMechanic(plan, timestamp);
|
||||
if (!mechanic) return;
|
||||
const items = (JOB_ABILITIES[row.dataset.job] ?? [])
|
||||
.filter(ab => !assignmentOverlapsJob(plan, row.dataset.job, ab.name, timestamp))
|
||||
.map(ab => ({
|
||||
label: `${plan.mitigationNames?.[ab.name] ?? ab.name} · ${fmtTimestamp(timestamp)}`,
|
||||
icon: MITIG_ICONS[ab.name] ?? '',
|
||||
onClick: () => addTimelineAssignment(planId, mechanic.id, ab.name, row.dataset.job, ab.buffType, timestamp),
|
||||
}));
|
||||
showTimelineMenu(e.clientX, e.clientY, items);
|
||||
const rowAbility = row.dataset.ability;
|
||||
const rowJob = row.dataset.job;
|
||||
const ab = (JOB_ABILITIES[rowJob] ?? []).find(a => a.name === rowAbility);
|
||||
if (!ab || assignmentOverlapsJob(plan, rowJob, rowAbility, timestamp)) return;
|
||||
addTimelineAssignment(planId, mechanic.id, rowAbility, rowJob, ab.buffType, timestamp);
|
||||
});
|
||||
|
||||
timeline.addEventListener('contextmenu', e => {
|
||||
@ -1023,6 +1037,7 @@ function initTimeline(planId) {
|
||||
const rect = track.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
|
||||
const timestamp = (x / rect.width) * planDurationMs(plan);
|
||||
if (row.dataset.ability && data.ability !== row.dataset.ability) return;
|
||||
updateTimelineAssignmentPosition(planId, data.mechanicId, data.ability, data.job, row.dataset.job, timestamp);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user