diff --git a/css/planner.css b/css/planner.css index 191674b..3a12bba 100644 --- a/css/planner.css +++ b/css/planner.css @@ -162,7 +162,7 @@ /* ── Mechanic Cards ──────────────────────────────────────────────────────────── */ .mechanic-card { display: grid; - grid-template-columns: 52px 1fr; + grid-template-columns: 52px 1fr auto; gap: 14px; padding: 12px 6px; border-bottom: 1px solid var(--border); @@ -175,6 +175,13 @@ .mechanic-card:last-child { border-bottom: none; } .mechanic-card:hover { background: var(--bg2); } +.mechanic-delete-btn { + opacity: 0; + transition: opacity 0.12s; + margin-top: 2px; +} +.mechanic-card:hover .mechanic-delete-btn { opacity: 1; } + .mechanic-edit-hint { font-size: 11px; color: var(--t3); @@ -229,17 +236,47 @@ } .badge-assign { + display: inline-flex; + align-items: center; + gap: 5px; background: var(--bg2); border: 1px solid var(--borderem); border-radius: var(--r); - padding: 2px 8px; - font-size: 12px; + padding: 4px 10px; + font-size: 13px; color: var(--t2); } .badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); } .badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); } .badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); } +.badge-assign--missing-job { + border-style: dashed; + opacity: 0.55; +} + +.badge-icon { + width: 20px; + height: 20px; + object-fit: contain; + flex-shrink: 0; +} + +.badge-remove { + background: none; + border: none; + color: inherit; + padding: 0; + margin-left: 2px; + cursor: pointer; + font-size: 13px; + line-height: 1; + opacity: 0; + transition: opacity 0.12s; +} +.badge-assign:hover .badge-remove { opacity: 0.6; } +.badge-remove:hover { opacity: 1 !important; } + .mechanic-notes { font-size: 12px; color: var(--t3); @@ -360,6 +397,9 @@ } .ability-chip { + display: inline-flex; + align-items: center; + gap: 5px; background: none; border: 1px solid var(--borderem); border-radius: var(--r); diff --git a/js/planner.js b/js/planner.js index 322b0c1..0664b0b 100644 --- a/js/planner.js +++ b/js/planner.js @@ -210,34 +210,44 @@ function renderMechanicListHtml(plan) { `; } - return plan.mechanics.map(m => ` -
-
${escHtml(fmtTimestamp(m.timestamp))}
-
- ${m.phase ? `
${escHtml(m.phase)}
` : ''} -
${escHtml(m.name)}
- ${m.unmitigatedDamage - ? `
${fmtNumber(m.unmitigatedDamage)} unmitigiert
` - : '' - } -
- ${m.assignments.length === 0 - ? 'Keine Zuweisung' - : m.assignments.map(a => { - const cls = a.buffType === 'debuff' ? 'badge-assign-debuff' - : a.buffType === 'shield' ? 'badge-assign-shield' - : a.buffType === 'buff' ? 'badge-assign-buff' - : ''; - const label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability); - return `${label}`; - }).join('') + const activeJobSet = new Set(plan.jobComposition.filter(j => j)); + + return plan.mechanics.map(m => { + const sorted = sortedAssignments(m.assignments); + const assignHtml = sorted.length === 0 + ? 'Keine Zuweisung' + : sorted.map(a => { + 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 label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability); + const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : ''; + return ` + ${icon ? `` : ''} + ${label} + + `; + }).join(''); + + return ` +
+
${escHtml(fmtTimestamp(m.timestamp))}
+
+ ${m.phase ? `
${escHtml(m.phase)}
` : ''} +
${escHtml(m.name)}
+ ${m.unmitigatedDamage + ? `
${fmtNumber(m.unmitigatedDamage)} unmitigiert
` + : '' } +
${assignHtml}
+ ${m.notes ? `
${escHtml(m.notes)}
` : ''} +
Klicken zum Bearbeiten
- ${m.notes ? `
${escHtml(m.notes)}
` : ''} -
Klicken zum Bearbeiten
-
-
- `).join(''); + +
`; + }).join(''); } // ── Job Slots ───────────────────────────────────────────────────────────────── @@ -276,6 +286,7 @@ function initJobSlots(planId) { const role = JOB_ROLE[sel.value] ?? ''; slot.className = 'job-slot' + (role ? ` job-slot--${role}` : ''); } + refreshMechanicList(planId); }); } @@ -288,10 +299,42 @@ function refreshMechanicList(planId) { if (el) el.innerHTML = renderMechanicListHtml(plan); } +function removeAssignment(planId, mechanicId, abilityName) { + const plan = getPlan(planId); + if (!plan) return; + const mechanic = plan.mechanics.find(m => m.id === mechanicId); + if (!mechanic) return; + mechanic.assignments = mechanic.assignments.filter(a => a.ability !== abilityName); + updatePlan(planId, { mechanics: plan.mechanics }); + refreshMechanicList(planId); + if (abilityModalMechanicId === mechanicId) renderAbilityModalContent(); +} + +function deleteMechanic(planId, mechanicId) { + const plan = getPlan(planId); + if (!plan) return; + plan.mechanics = plan.mechanics.filter(m => m.id !== mechanicId); + updatePlan(planId, { mechanics: plan.mechanics }); + refreshMechanicList(planId); + renderPlanList(); +} + function initMechanicClicks(planId) { const list = document.getElementById('mechanic-list'); if (!list) return; list.addEventListener('click', e => { + const removeBtn = e.target.closest('.badge-remove'); + if (removeBtn) { + e.stopPropagation(); + removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability); + return; + } + const deleteBtn = e.target.closest('.mechanic-delete-btn'); + if (deleteBtn) { + e.stopPropagation(); + deleteMechanic(planId, deleteBtn.dataset.mechanicId); + return; + } const card = e.target.closest('.mechanic-card'); if (!card) return; showAbilityModal(planId, card.dataset.mechanicId); @@ -507,6 +550,56 @@ const JOB_ABILITIES = { ], }; +const MITIG_ICONS = { + 'Passage of Arms': 'assets/icons/mitigation/passage-of-arms.png', + 'Dark Missionary': 'assets/icons/mitigation/dark-missionary.png', + 'Heart of Light': 'assets/icons/mitigation/heart-of-light.png', + 'Temperance': 'assets/icons/mitigation/temperance.png', + 'Sacred Soil': 'assets/icons/mitigation/sacred-soil.png', + 'Expedient': 'assets/icons/mitigation/expedient.png', + 'Fey Illumination': 'assets/icons/mitigation/fey-illumination.png', + 'Collective Unconscious': 'assets/icons/mitigation/collective-unconscious.png', + 'Holos': 'assets/icons/mitigation/holos.png', + 'Kerachole': 'assets/icons/mitigation/kerachole.png', + 'Troubadour': 'assets/icons/mitigation/troubadour.png', + 'Tactician': 'assets/icons/mitigation/tactician.png', + 'Shield Samba': 'assets/icons/mitigation/shield-samba.png', + 'Magick Barrier': 'assets/icons/mitigation/magick-barrier.png', + 'Reprisal': 'assets/icons/mitigation/reprisal.png', + 'Feint': 'assets/icons/mitigation/feint.png', + 'Addle': 'assets/icons/mitigation/addle.png', + 'Divine Veil': 'assets/icons/mitigation/divine-veil.png', + 'Guardian': 'assets/icons/mitigation/guardian.png', + 'Shake It Off': 'assets/icons/mitigation/shake-it-off.png', + 'Bloodwhetting': 'assets/icons/mitigation/bloodwhetting.png', + 'Divine Benison': 'assets/icons/mitigation/divine-benison.png', + 'Divine Caress': 'assets/icons/mitigation/divine-caress.png', + 'Intersection': 'assets/icons/mitigation/intersection.png', + 'Neutral Sect': 'assets/icons/mitigation/neutral-sect.png', + 'the Spire': 'assets/icons/mitigation/the-spire.png', + 'Panhaima': 'assets/icons/mitigation/panhaima.png', + 'Holosakos': 'assets/icons/mitigation/holos.png', + 'Eukrasian Prognosis': 'assets/icons/mitigation/eukrasian-prognosis.png', + 'Eukrasian Prognosis II': 'assets/icons/mitigation/eukrasian-prognosis-ii.png', + 'Eukrasian Diagnosis': 'assets/icons/mitigation/eukrasian-diagnosis.png', + 'Differential Diagnosis': 'assets/icons/mitigation/eukrasian-diagnosis.png', + 'Haima': 'assets/icons/mitigation/haima.png', + 'Galvanize': 'assets/icons/mitigation/galvanize.png', + 'Seraphic Veil': 'assets/icons/mitigation/seraphic-veil.png', + 'Radiant Aegis': 'assets/icons/mitigation/radiant-aegis.png', + 'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png', + 'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png', + 'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png', +}; + +const ASSIGN_ORDER = { debuff: 0, buff: 1, shield: 2 }; + +function sortedAssignments(assignments) { + return [...assignments].sort((a, b) => + (ASSIGN_ORDER[a.buffType] ?? 1) - (ASSIGN_ORDER[b.buffType] ?? 1) + ); +} + function guessJob(abilityName, players) { if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName]; const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? ''); @@ -569,6 +662,19 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga // ── Merge + Create plan from import ────────────────────────────────────────── +function extractJobComp(players) { + const order = { tank: 0, healer: 1, dps: 2 }; + const sorted = [...(players ?? [])] + .filter(p => JOB_FROM_TYPE[p.type]) + .sort((a, b) => { + const roleCmp = (order[a.role] ?? 2) - (order[b.role] ?? 2); + return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name); + }); + const comp = sorted.map(p => JOB_FROM_TYPE[p.type] ?? '').slice(0, 8); + while (comp.length < 8) comp.push(''); + return comp; +} + function doImport(data, withMitigations, whereMode, mergeId, newName) { const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data; const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations); @@ -577,7 +683,8 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) { const plan = createPlan(newName || fightName || 'Importierter Plan'); return updatePlan(plan.id, { mechanics, - source: { reportCode, fightName }, + source: { reportCode, fightName }, + jobComposition: extractJobComp(players), }); } @@ -649,12 +756,13 @@ function renderAbilityModalContent() { const activeClass = isActive ? ' ability-chip--active' : ''; const otherClass = byOtherJob ? ' ability-chip--other-job' : ''; const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : ''; + const icon = MITIG_ICONS[ab.name] ?? ''; return ``; + >${icon ? `` : ''}${escHtml(ab.name)}`; }).join(''); return `