diff --git a/css/planner.css b/css/planner.css index c06fbb9..267260b 100644 --- a/css/planner.css +++ b/css/planner.css @@ -224,10 +224,11 @@ } .mechanic-assignments { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 2px; + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 2px; + align-items: flex-start; } .mechanic-no-assign { @@ -417,6 +418,39 @@ .ability-chip--other-job { opacity: 0.45; } +/* ── Equivalents hint ────────────────────────────────────────────────────────── */ +.badge-with-hint { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.badge-equiv-hint { + font-size: 11px; + color: var(--green); + white-space: nowrap; + padding: 0 2px; + cursor: default; +} + +.badge-no-equiv-hint { + font-size: 11px; + color: var(--red); + white-space: nowrap; + padding: 0 2px; + cursor: default; +} + +.badge-job-hint { + font-size: 11px; + color: var(--t3); + white-space: nowrap; + padding: 0 2px; + cursor: default; + font-style: italic; +} + /* ── Folder Sidebar ──────────────────────────────────────────────────────────── */ .folder-section { margin-bottom: 2px; } diff --git a/js/planner.js b/js/planner.js index 91b9ae9..68fc214 100644 --- a/js/planner.js +++ b/js/planner.js @@ -370,16 +370,27 @@ function renderMechanicListHtml(plan) { const cls = a.buffType === 'debuff' ? 'badge-assign-debuff' : a.buffType === 'shield' ? 'badge-assign-shield' : 'badge-assign-buff'; - const isMissing = !!a.job && !activeJobSet.has(a.job); - const icon = MITIG_ICONS[a.ability] ?? ''; - const ability = assignmentAbilityName(a, plan); - const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability); - const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : ''; - return ` + const isMissing = !!a.job && !activeJobSet.has(a.job); + const icon = MITIG_ICONS[a.ability] ?? ''; + const ability = assignmentAbilityName(a, plan); + const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability); + const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : ''; + const suggestions = isMissing ? findEquivSuggestions(a.ability, activeJobSet) : []; + const badgeHtml = ` ${icon ? `` : ''} ${label} `; + const hintHtml = suggestions.map(s => + `→ ${escHtml(s.ability)} (${escHtml(s.job)})?` + ).join(''); + const noEquivHint = isMissing && suggestions.length === 0 + ? `→ Kein Äquivalent!` : ''; + const jobHint = !a.job ? `→ Job zuordnen` : ''; + const needsWrap = suggestions.length > 0 || !!noEquivHint || !!jobHint; + return needsWrap + ? `${badgeHtml}${hintHtml}${noEquivHint}${jobHint}` + : badgeHtml; }).join(''); return ` @@ -843,6 +854,29 @@ const MITIG_ICONS = { 'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png', }; +// Groups of abilities that are functionally equivalent across different jobs. +// Used to suggest replacements when a job is missing from the composition. +const ABILITY_EQUIVALENTS = [ + ['Troubadour', 'Tactician', 'Shield Samba'], + ['Sacred Soil', 'Kerachole'], +]; + +function findEquivSuggestions(abilityName, activeJobSet) { + const group = ABILITY_EQUIVALENTS.find(g => g.includes(abilityName)); + if (!group) return []; + const suggestions = []; + for (const equiv of group) { + if (equiv === abilityName) continue; + for (const job of activeJobSet) { + if ((JOB_ABILITIES[job] ?? []).some(a => a.name === equiv)) { + suggestions.push({ ability: equiv, job }); + break; + } + } + } + return suggestions; +} + const ASSIGN_ORDER = { debuff: 0, buff: 1, shield: 2 }; function sortedAssignments(assignments) {