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;
|
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 {
|
.timeline-boss-action {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
@ -848,16 +875,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-mitigation-active {
|
.timeline-mitigation-active { display: none; }
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: var(--active-width);
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.22;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-mitigation:active { cursor: grabbing; }
|
.timeline-mitigation:active { cursor: grabbing; }
|
||||||
.timeline-mitigation.selected {
|
.timeline-mitigation.selected {
|
||||||
@ -891,9 +909,21 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-mitigation--buff { border-color: rgba(200,168,75,.5); color: var(--gold); background: rgba(200,168,75,.12); }
|
.timeline-mitigation--buff {
|
||||||
.timeline-mitigation--debuff { border-color: rgba(224,92,92,.5); color: var(--red); background: rgba(224,92,92,.12); }
|
border-color: rgba(200,168,75,.5);
|
||||||
.timeline-mitigation--shield { border-color: rgba(74,158,255,.5); color: var(--blue); background: rgba(74,158,255,.12); }
|
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 {
|
.timeline-axis {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
|||||||
@ -582,12 +582,18 @@ function timelineScale(plan) {
|
|||||||
|
|
||||||
function timelinePlayerRows(plan) {
|
function timelinePlayerRows(plan) {
|
||||||
const roster = plan.playerRoster ?? [];
|
const roster = plan.playerRoster ?? [];
|
||||||
return (plan.jobComposition ?? []).map((job, idx) => ({
|
const rows = [];
|
||||||
idx,
|
(plan.jobComposition ?? []).forEach((job, idx) => {
|
||||||
job,
|
if (!job) return;
|
||||||
name: roster[idx]?.name ?? '',
|
const name = roster[idx]?.name ?? '';
|
||||||
role: JOB_ROLE[job] ?? '',
|
const role = JOB_ROLE[job] ?? '';
|
||||||
})).filter(row => row.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) {
|
function selectedAssignmentMatches(mechanicId, assignment) {
|
||||||
@ -603,8 +609,11 @@ function jobCanUseAbility(job, ability) {
|
|||||||
|
|
||||||
function timelineRowsForAssignment(rows, assignment) {
|
function timelineRowsForAssignment(rows, assignment) {
|
||||||
const assignedJob = assignment.job ?? '';
|
const assignedJob = assignment.job ?? '';
|
||||||
if (assignedJob) return rows.filter(row => row.job === assignedJob);
|
return rows.filter(row => {
|
||||||
return rows.filter(row => jobCanUseAbility(row.job, assignment.ability));
|
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) {
|
function assignmentStartMs(mechanic, assignment) {
|
||||||
@ -707,7 +716,9 @@ function renderTimelineHtml(plan) {
|
|||||||
const blocks = [];
|
const blocks = [];
|
||||||
for (const m of mechanics) {
|
for (const m of mechanics) {
|
||||||
for (const a of sortedAssignments(m.assignments ?? [])) {
|
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 start = Number.isFinite(Number(a.timestamp)) ? Number(a.timestamp) : m.timestamp;
|
||||||
const durationSec = assignmentDurationSeconds(a);
|
const durationSec = assignmentDurationSeconds(a);
|
||||||
const cooldownSec = assignmentCooldownSeconds(a);
|
const cooldownSec = assignmentCooldownSeconds(a);
|
||||||
@ -721,7 +732,7 @@ function renderTimelineHtml(plan) {
|
|||||||
: a.buffType === 'shield' ? 'timeline-mitigation--shield'
|
: a.buffType === 'shield' ? 'timeline-mitigation--shield'
|
||||||
: 'timeline-mitigation--buff';
|
: 'timeline-mitigation--buff';
|
||||||
const icon = MITIG_ICONS[a.ability] ?? '';
|
const icon = MITIG_ICONS[a.ability] ?? '';
|
||||||
const ability = assignmentAbilityName(a, plan);
|
const abilityLabel = assignmentAbilityName(a, plan);
|
||||||
blocks.push(`
|
blocks.push(`
|
||||||
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
|
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@ -729,19 +740,25 @@ 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(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>
|
<span class="timeline-mitigation-active"></span>
|
||||||
${icon ? `<img src="${escHtml(icon)}" alt="">` : ''}
|
${icon ? `<img src="${escHtml(icon)}" alt="">` : ''}
|
||||||
<span>${escHtml(ability)}</span>
|
<span>${escHtml(abilityLabel)}</span>
|
||||||
</button>
|
</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 `
|
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">
|
<div class="timeline-row-label">
|
||||||
<span class="timeline-job role-${escHtml(row.role)}">${escHtml(row.job)}</span>
|
<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>
|
||||||
<div class="timeline-track" style="width:${width}px">${hitLines}${blocks.join('')}</div>
|
<div class="timeline-track" style="width:${width}px">${hitLines}${blocks.join('')}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -977,14 +994,11 @@ function initTimeline(planId) {
|
|||||||
const timestamp = timelineTimestampFromEvent(plan, track, e);
|
const timestamp = timelineTimestampFromEvent(plan, track, e);
|
||||||
const mechanic = findNearestMechanic(plan, timestamp);
|
const mechanic = findNearestMechanic(plan, timestamp);
|
||||||
if (!mechanic) return;
|
if (!mechanic) return;
|
||||||
const items = (JOB_ABILITIES[row.dataset.job] ?? [])
|
const rowAbility = row.dataset.ability;
|
||||||
.filter(ab => !assignmentOverlapsJob(plan, row.dataset.job, ab.name, timestamp))
|
const rowJob = row.dataset.job;
|
||||||
.map(ab => ({
|
const ab = (JOB_ABILITIES[rowJob] ?? []).find(a => a.name === rowAbility);
|
||||||
label: `${plan.mitigationNames?.[ab.name] ?? ab.name} · ${fmtTimestamp(timestamp)}`,
|
if (!ab || assignmentOverlapsJob(plan, rowJob, rowAbility, timestamp)) return;
|
||||||
icon: MITIG_ICONS[ab.name] ?? '',
|
addTimelineAssignment(planId, mechanic.id, rowAbility, rowJob, ab.buffType, timestamp);
|
||||||
onClick: () => addTimelineAssignment(planId, mechanic.id, ab.name, row.dataset.job, ab.buffType, timestamp),
|
|
||||||
}));
|
|
||||||
showTimelineMenu(e.clientX, e.clientY, items);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
timeline.addEventListener('contextmenu', e => {
|
timeline.addEventListener('contextmenu', e => {
|
||||||
@ -1023,6 +1037,7 @@ function initTimeline(planId) {
|
|||||||
const rect = track.getBoundingClientRect();
|
const rect = track.getBoundingClientRect();
|
||||||
const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
|
const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
|
||||||
const timestamp = (x / rect.width) * planDurationMs(plan);
|
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);
|
updateTimelineAssignmentPosition(planId, data.mechanicId, data.ability, data.job, row.dataset.job, timestamp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user