From da226d54a27cc905f9cbe0ed38fa2f70ea8c914e Mon Sep 17 00:00:00 2001 From: xziino Date: Sat, 23 May 2026 17:15:58 +0200 Subject: [PATCH] Planer: Gantt-Zeilen pro Ability + aktive Dauer im Balken sichtbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- css/planner.css | 56 +++++++++++++++++++++++++++++++++++----------- js/planner.js | 59 +++++++++++++++++++++++++++++++------------------ 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/css/planner.css b/css/planner.css index 8236c3f..02aa87a 100644 --- a/css/planner.css +++ b/css/planner.css @@ -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; diff --git a/js/planner.js b/js/planner.js index 346a1ac..29833ef 100644 --- a/js/planner.js +++ b/js/planner.js @@ -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(` `); } } + const icon = MITIG_ICONS[row.ability] ?? ''; + const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability; + const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : ''; return ` -
+
${escHtml(row.job)} - ${escHtml(row.name || `Slot ${row.idx + 1}`)} + + ${icon ? `` : ''} + ${escHtml(abilityDisplayName)} +
${hitLines}${blocks.join('')}
`; @@ -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); });