From 8f29619ef5141fa143ac6dc35b368b1ade72d748 Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 10:23:47 +0200 Subject: [PATCH 1/2] Planner Steps 4+5: job composition slots and ability assignment modal Co-Authored-By: Claude Sonnet 4.6 --- css/planner.css | 87 ++++++++++++-- js/planner.js | 279 ++++++++++++++++++++++++++++++++++++++++++++- templates/page.php | 11 ++ 3 files changed, 362 insertions(+), 15 deletions(-) diff --git a/css/planner.css b/css/planner.css index c517d57..191674b 100644 --- a/css/planner.css +++ b/css/planner.css @@ -137,27 +137,52 @@ color: var(--t3); } -/* ── Job Slots Placeholder ───────────────────────────────────────────────────── */ -.job-slots-placeholder { - padding: 20px; - text-align: center; - color: var(--t3); - font-size: 13px; - background: var(--bg2); - border: 1px dashed var(--border); - border-radius: var(--r); +/* ── Job Slots ────────────────────────────────────────────────────────────────── */ +.job-slots-grid { + display: flex; + gap: 8px; } +.job-slot { + flex: 1; + min-width: 0; +} + +.job-slot select { + width: 100%; + font-size: 13px; + padding: 5px 6px; + border-left: 2px solid var(--border); +} + +.job-slot--tank select { border-left-color: var(--blue); } +.job-slot--healer select { border-left-color: var(--green); } +.job-slot--dps select { border-left-color: rgba(200,168,75,.5); } + /* ── Mechanic Cards ──────────────────────────────────────────────────────────── */ .mechanic-card { display: grid; grid-template-columns: 52px 1fr; gap: 14px; - padding: 12px 0; + padding: 12px 6px; border-bottom: 1px solid var(--border); align-items: start; + cursor: pointer; + transition: background 0.12s; + border-radius: var(--r); + margin: 0 -6px; } .mechanic-card:last-child { border-bottom: none; } +.mechanic-card:hover { background: var(--bg2); } + +.mechanic-edit-hint { + font-size: 11px; + color: var(--t3); + margin-top: 2px; + opacity: 0; + transition: opacity 0.12s; +} +.mechanic-card:hover .mechanic-edit-hint { opacity: 1; } .mechanic-time { font-family: var(--font-d); @@ -309,3 +334,45 @@ gap: 8px; margin-top: 6px; } + +/* ── Ability Assignment Modal ────────────────────────────────────────────────── */ +.ability-modal-box { + max-width: 640px; + max-height: 80vh; + overflow-y: auto; +} + +.ability-job-group { + margin-bottom: 14px; +} + +.ability-job-label { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.ability-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ability-chip { + background: none; + border: 1px solid var(--borderem); + border-radius: var(--r); + padding: 4px 10px; + font-size: 12px; + color: var(--t2); + cursor: pointer; + transition: all 0.12s; +} +.ability-chip:hover { background: var(--bg3); color: var(--t1); } + +.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); } +.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); } +.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); } + +.ability-chip--other-job { opacity: 0.45; } diff --git a/js/planner.js b/js/planner.js index deca3bd..322b0c1 100644 --- a/js/planner.js +++ b/js/planner.js @@ -175,8 +175,8 @@ function renderPlanDetail(plan) {
Jobaufstellung
-
- Jobaufstellung wird in einem späteren Schritt konfigurierbar +
+ ${renderJobSlotsHtml(plan)}
@@ -184,16 +184,20 @@ function renderPlanDetail(plan) {
Mechaniken
- ${renderMechanicList(plan)} +
+ ${renderMechanicListHtml(plan)} +
`; document.getElementById('plan-name-edit-btn')?.addEventListener('click', () => { startRename(plan.id, plan.name); }); + initJobSlots(plan.id); + initMechanicClicks(plan.id); } -function renderMechanicList(plan) { +function renderMechanicListHtml(plan) { if (plan.mechanics.length === 0) { return `
@@ -207,7 +211,7 @@ function renderMechanicList(plan) { } return plan.mechanics.map(m => ` -
+
${escHtml(fmtTimestamp(m.timestamp))}
${m.phase ? `
${escHtml(m.phase)}
` : ''} @@ -230,11 +234,70 @@ function renderMechanicList(plan) { }
${m.notes ? `
${escHtml(m.notes)}
` : ''} +
Klicken zum Bearbeiten
`).join(''); } +// ── Job Slots ───────────────────────────────────────────────────────────────── + +function renderJobSlotsHtml(plan) { + return Array.from({ length: 8 }, (_, i) => { + const job = plan.jobComposition[i] ?? ''; + const role = JOB_ROLE[job] ?? ''; + return ` +
+ +
`; + }).join(''); +} + +function initJobSlots(planId) { + const grid = document.getElementById('job-slots-grid'); + if (!grid) return; + grid.addEventListener('change', e => { + const sel = e.target.closest('.job-slot-select'); + if (!sel) return; + const plan = getPlan(planId); + if (!plan) return; + const comp = [...plan.jobComposition]; + comp[parseInt(sel.dataset.idx, 10)] = sel.value; + updatePlan(planId, { jobComposition: comp }); + const slot = sel.closest('.job-slot'); + if (slot) { + const role = JOB_ROLE[sel.value] ?? ''; + slot.className = 'job-slot' + (role ? ` job-slot--${role}` : ''); + } + }); +} + +// ── Mechanic list helpers ───────────────────────────────────────────────────── + +function refreshMechanicList(planId) { + const plan = getPlan(planId); + if (!plan) return; + const el = document.getElementById('mechanic-list'); + if (el) el.innerHTML = renderMechanicListHtml(plan); +} + +function initMechanicClicks(planId) { + const list = document.getElementById('mechanic-list'); + if (!list) return; + list.addEventListener('click', e => { + const card = e.target.closest('.mechanic-card'); + if (!card) return; + showAbilityModal(planId, card.dataset.mechanicId); + }); +} + // ── Rename ──────────────────────────────────────────────────────────────────── function startRename(id, currentName) { @@ -348,6 +411,102 @@ const TANK_JOBS = new Set(['PLD', 'WAR', 'DRK', 'GNB']); const MELEE_JOBS = new Set(['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR']); const CASTER_JOBS = new Set(['BLM', 'SMN', 'RDM', 'PCT']); +const JOB_ROLE = { + 'PLD': 'tank', 'WAR': 'tank', 'DRK': 'tank', 'GNB': 'tank', + 'WHM': 'healer', 'SCH': 'healer', 'AST': 'healer', 'SGE': 'healer', + 'MNK': 'dps', 'DRG': 'dps', 'NIN': 'dps', 'SAM': 'dps', + 'RPR': 'dps', 'VPR': 'dps', 'BRD': 'dps', 'MCH': 'dps', + 'DNC': 'dps', 'BLM': 'dps', 'SMN': 'dps', 'RDM': 'dps', 'PCT': 'dps', +}; + +const ALL_JOBS = [ + { group: 'Tank', jobs: ['PLD', 'WAR', 'DRK', 'GNB'] }, + { group: 'Healer', jobs: ['WHM', 'SCH', 'AST', 'SGE'] }, + { group: 'Melee', jobs: ['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR'] }, + { group: 'Ranged', jobs: ['BRD', 'MCH', 'DNC'] }, + { group: 'Caster', jobs: ['BLM', 'SMN', 'RDM', 'PCT'] }, +]; + +const JOB_ABILITIES = { + 'PLD': [ + { name: 'Passage of Arms', buffType: 'buff' }, + { name: 'Divine Veil', buffType: 'shield' }, + { name: 'Guardian', buffType: 'shield' }, + { name: 'Reprisal', buffType: 'debuff' }, + ], + 'WAR': [ + { name: 'Shake It Off', buffType: 'shield' }, + { name: 'Bloodwhetting', buffType: 'shield' }, + { name: 'Reprisal', buffType: 'debuff' }, + ], + 'DRK': [ + { name: 'Dark Missionary', buffType: 'buff' }, + { name: 'Reprisal', buffType: 'debuff' }, + ], + 'GNB': [ + { name: 'Heart of Light', buffType: 'buff' }, + { name: 'Reprisal', buffType: 'debuff' }, + ], + 'WHM': [ + { name: 'Temperance', buffType: 'buff' }, + { name: 'Divine Benison', buffType: 'shield' }, + { name: 'Divine Caress', buffType: 'shield' }, + ], + 'SCH': [ + { name: 'Sacred Soil', buffType: 'buff' }, + { name: 'Expedient', buffType: 'buff' }, + { name: 'Fey Illumination', buffType: 'buff' }, + { name: 'Galvanize', buffType: 'shield' }, + { name: 'Seraphic Veil', buffType: 'shield' }, + { name: 'Catalyze', buffType: 'shield' }, + { name: 'Addle', buffType: 'debuff' }, + ], + 'AST': [ + { name: 'Collective Unconscious', buffType: 'buff' }, + { name: 'Neutral Sect', buffType: 'shield' }, + { name: 'Intersection', buffType: 'shield' }, + { name: 'the Spire', buffType: 'shield' }, + ], + 'SGE': [ + { name: 'Kerachole', buffType: 'buff' }, + { name: 'Holos', buffType: 'buff' }, + { name: 'Holosakos', buffType: 'shield' }, + { name: 'Panhaima', buffType: 'shield' }, + { name: 'Eukrasian Prognosis', buffType: 'shield' }, + { name: 'Eukrasian Prognosis II', buffType: 'shield' }, + { name: 'Eukrasian Diagnosis', buffType: 'shield' }, + { name: 'Differential Diagnosis', buffType: 'shield' }, + { name: 'Haima', buffType: 'shield' }, + { name: 'Addle', buffType: 'debuff' }, + ], + 'BRD': [{ name: 'Troubadour', buffType: 'buff' }], + 'MCH': [{ name: 'Tactician', buffType: 'buff' }], + 'DNC': [ + { name: 'Shield Samba', buffType: 'buff' }, + { name: 'Improvised Finish', buffType: 'shield' }, + ], + 'MNK': [{ name: 'Feint', buffType: 'debuff' }], + 'DRG': [{ name: 'Feint', buffType: 'debuff' }], + 'NIN': [{ name: 'Feint', buffType: 'debuff' }], + 'SAM': [{ name: 'Feint', buffType: 'debuff' }], + 'RPR': [{ name: 'Feint', buffType: 'debuff' }], + 'VPR': [{ name: 'Feint', buffType: 'debuff' }], + 'BLM': [{ name: 'Addle', buffType: 'debuff' }], + 'SMN': [ + { name: 'Addle', buffType: 'debuff' }, + { name: 'Radiant Aegis', buffType: 'shield' }, + ], + 'RDM': [ + { name: 'Addle', buffType: 'debuff' }, + { name: 'Magick Barrier', buffType: 'buff' }, + ], + 'PCT': [ + { name: 'Addle', buffType: 'debuff' }, + { name: 'Tempera Coat', buffType: 'shield' }, + { name: 'Tempera Grassa', buffType: 'shield' }, + ], +}; + function guessJob(abilityName, players) { if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName]; const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? ''); @@ -438,6 +597,115 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) { return updatePlan(mergeId, { mechanics: merged }); } +// ── Ability Assignment Modal ────────────────────────────────────────────────── + +let abilityModalPlanId = null; +let abilityModalMechanicId = null; + +function showAbilityModal(planId, mechanicId) { + abilityModalPlanId = planId; + abilityModalMechanicId = mechanicId; + renderAbilityModalContent(); + document.getElementById('planner-ability-modal').style.display = 'flex'; +} + +function hideAbilityModal() { + document.getElementById('planner-ability-modal').style.display = 'none'; + abilityModalPlanId = null; + abilityModalMechanicId = null; +} + +function renderAbilityModalContent() { + const plan = getPlan(abilityModalPlanId); + if (!plan) return; + const mechanic = plan.mechanics.find(m => m.id === abilityModalMechanicId); + if (!mechanic) return; + + document.getElementById('ability-modal-title').textContent = mechanic.name; + + const activeJobs = [...new Set(plan.jobComposition.filter(j => j))]; + const content = document.getElementById('ability-modal-content'); + + if (activeJobs.length === 0) { + content.innerHTML = ` +

+ Bitte zuerst die Jobaufstellung konfigurieren. +

`; + return; + } + + content.innerHTML = activeJobs.map(job => { + const abilities = JOB_ABILITIES[job] ?? []; + if (!abilities.length) return ''; + const role = JOB_ROLE[job] ?? 'dps'; + + const chips = abilities.map(ab => { + const assigned = mechanic.assignments.find(a => a.ability === ab.name); + const isActive = !!assigned; + const byOtherJob = isActive && assigned.job !== job; + const cls = ab.buffType === 'debuff' ? 'badge-assign-debuff' + : ab.buffType === 'shield' ? 'badge-assign-shield' + : 'badge-assign-buff'; + const activeClass = isActive ? ' ability-chip--active' : ''; + const otherClass = byOtherJob ? ' ability-chip--other-job' : ''; + const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : ''; + return ``; + }).join(''); + + return ` +
+
+ ${escHtml(job)} +
+
${chips}
+
`; + }).join(''); +} + +function toggleAbilityAssignment(abilityName, job, buffType) { + const plan = getPlan(abilityModalPlanId); + if (!plan) return; + const mechanic = plan.mechanics.find(m => m.id === abilityModalMechanicId); + if (!mechanic) return; + + const idx = mechanic.assignments.findIndex(a => a.ability === abilityName); + if (idx !== -1) { + if (mechanic.assignments[idx].job === job) { + mechanic.assignments.splice(idx, 1); + } else { + mechanic.assignments[idx].job = job; + } + } else { + mechanic.assignments.push({ ability: abilityName, job, buffType }); + } + + updatePlan(abilityModalPlanId, { mechanics: plan.mechanics }); + refreshMechanicList(abilityModalPlanId); + renderAbilityModalContent(); +} + +function initAbilityModal() { + const overlay = document.getElementById('planner-ability-modal'); + if (!overlay) return; + + document.getElementById('ability-modal-close')?.addEventListener('click', hideAbilityModal); + overlay.addEventListener('click', e => { if (e.target === overlay) hideAbilityModal(); }); + document.addEventListener('keydown', e => { + if (e.key === 'Escape' && abilityModalPlanId) hideAbilityModal(); + }); + + document.getElementById('ability-modal-content')?.addEventListener('click', e => { + const btn = e.target.closest('.ability-chip'); + if (!btn) return; + toggleAbilityAssignment(btn.dataset.ability, btn.dataset.job, btn.dataset.buffType); + }); +} + // ── Import Modal ────────────────────────────────────────────────────────────── let pendingImportData = null; @@ -553,6 +821,7 @@ window.plannerTab = { document.addEventListener('DOMContentLoaded', () => { initNewPlanForm(); initImportModal(); + initAbilityModal(); renderPlanList(); renderPlanDetail(null); }); diff --git a/templates/page.php b/templates/page.php index 1f2eadb..17e3bc9 100644 --- a/templates/page.php +++ b/templates/page.php @@ -80,6 +80,17 @@
+ + + From 969484a1dc59f6923a5104a28750a1144cb817fc Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 10:43:25 +0200 Subject: [PATCH 2/2] Planner: assignment sorting, icons, remove buttons, missing-job highlight, mechanic delete, job pre-fill on import Co-Authored-By: Claude Sonnet 4.6 --- css/planner.css | 46 +++++++++++++- js/planner.js | 164 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 179 insertions(+), 31 deletions(-) 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 `