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 `