Merge remote-tracking branch 'origin/master' into akus_schabernack3

# Conflicts:
#	js/planner.js
This commit is contained in:
Akurosia Kamo 2026-05-22 10:52:41 +02:00
commit 7c6f443a53
3 changed files with 540 additions and 44 deletions

View File

@ -137,27 +137,59 @@
color: var(--t3); color: var(--t3);
} }
/* ── Job Slots Placeholder ───────────────────────────────────────────────────── */ /* ── Job Slots ────────────────────────────────────────────────────────────────── */
.job-slots-placeholder { .job-slots-grid {
padding: 20px; display: flex;
text-align: center; gap: 8px;
color: var(--t3);
font-size: 13px;
background: var(--bg2);
border: 1px dashed var(--border);
border-radius: var(--r);
} }
.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 Cards ──────────────────────────────────────────────────────────── */
.mechanic-card { .mechanic-card {
display: grid; display: grid;
grid-template-columns: 52px 1fr; grid-template-columns: 52px 1fr auto;
gap: 14px; gap: 14px;
padding: 12px 0; padding: 12px 6px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
align-items: start; 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: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);
margin-top: 2px;
opacity: 0;
transition: opacity 0.12s;
}
.mechanic-card:hover .mechanic-edit-hint { opacity: 1; }
.mechanic-time { .mechanic-time {
font-family: var(--font-d); font-family: var(--font-d);
@ -204,17 +236,47 @@
} }
.badge-assign { .badge-assign {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--bg2); background: var(--bg2);
border: 1px solid var(--borderem); border: 1px solid var(--borderem);
border-radius: var(--r); border-radius: var(--r);
padding: 2px 8px; padding: 4px 10px;
font-size: 12px; font-size: 13px;
color: var(--t2); color: var(--t2);
} }
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); } .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-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-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 { .mechanic-notes {
font-size: 12px; font-size: 12px;
color: var(--t3); color: var(--t3);
@ -309,3 +371,48 @@
gap: 8px; gap: 8px;
margin-top: 6px; 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 {
display: inline-flex;
align-items: center;
gap: 5px;
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; }

View File

@ -199,8 +199,8 @@ function renderPlanDetail(plan) {
<div class="card section-gap"> <div class="card section-gap">
<div class="card-title">Jobaufstellung</div> <div class="card-title">Jobaufstellung</div>
<div class="job-slots-placeholder"> <div class="job-slots-grid" id="job-slots-grid">
Jobaufstellung wird in einem späteren Schritt konfigurierbar ${renderJobSlotsHtml(plan)}
</div> </div>
</div> </div>
@ -208,16 +208,20 @@ function renderPlanDetail(plan) {
<div class="card-title-row"> <div class="card-title-row">
<div class="card-title">Mechaniken</div> <div class="card-title">Mechaniken</div>
</div> </div>
${renderMechanicList(plan)} <div id="mechanic-list">
${renderMechanicListHtml(plan)}
</div>
</div> </div>
`; `;
document.getElementById('plan-name-edit-btn')?.addEventListener('click', () => { document.getElementById('plan-name-edit-btn')?.addEventListener('click', () => {
startRename(plan.id, plan.name); startRename(plan.id, plan.name);
}); });
initJobSlots(plan.id);
initMechanicClicks(plan.id);
} }
function renderMechanicList(plan) { function renderMechanicListHtml(plan) {
if (plan.mechanics.length === 0) { if (plan.mechanics.length === 0) {
return ` return `
<div class="empty" style="padding:30px 0"> <div class="empty" style="padding:30px 0">
@ -230,8 +234,30 @@ function renderMechanicList(plan) {
`; `;
} }
return plan.mechanics.map(m => ` const activeJobSet = new Set(plan.jobComposition.filter(j => j));
<div class="mechanic-card">
return plan.mechanics.map(m => {
const sorted = sortedAssignments(m.assignments);
const assignHtml = sorted.length === 0
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
: 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 ability = localizedAbilityName(a.ability, a.abilityName ?? a.ability);
const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability);
const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : '';
return `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}>
${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}
${label}
<button class="badge-remove" data-mechanic-id="${escHtml(m.id)}" data-ability="${escHtml(a.ability)}" title="Entfernen">×</button>
</span>`;
}).join('');
return `
<div class="mechanic-card" data-mechanic-id="${escHtml(m.id)}">
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div> <div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
<div class="mechanic-body"> <div class="mechanic-body">
${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''} ${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''}
@ -240,24 +266,104 @@ function renderMechanicList(plan) {
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert</div>` ? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert</div>`
: '' : ''
} }
<div class="mechanic-assignments"> <div class="mechanic-assignments">${assignHtml}</div>
${m.assignments.length === 0
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
: 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 ability = localizedAbilityName(a.ability, a.abilityName ?? a.ability);
const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability);
return `<span class="badge badge-assign ${cls}">${label}</span>`;
}).join('')
}
</div>
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''} ${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
<div class="mechanic-edit-hint">Klicken zum Bearbeiten</div>
</div> </div>
</div> <button class="mechanic-delete-btn plan-btn plan-btn-danger" data-mechanic-id="${escHtml(m.id)}" title="Mechanik löschen"></button>
`).join(''); </div>`;
}).join('');
}
// ── Job Slots ─────────────────────────────────────────────────────────────────
function renderJobSlotsHtml(plan) {
return Array.from({ length: 8 }, (_, i) => {
const job = plan.jobComposition[i] ?? '';
const role = JOB_ROLE[job] ?? '';
return `
<div class="job-slot${role ? ` job-slot--${role}` : ''}">
<select class="job-slot-select" data-idx="${i}">
<option value=""></option>
${ALL_JOBS.map(g =>
`<optgroup label="${escHtml(g.group)}">${g.jobs.map(j =>
`<option value="${j}"${j === job ? ' selected' : ''}>${j}</option>`
).join('')}</optgroup>`
).join('')}
</select>
</div>`;
}).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}` : '');
}
refreshMechanicList(planId);
});
}
// ── 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 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);
});
} }
// ── Rename ──────────────────────────────────────────────────────────────────── // ── Rename ────────────────────────────────────────────────────────────────────
@ -373,6 +479,152 @@ const TANK_JOBS = new Set(['PLD', 'WAR', 'DRK', 'GNB']);
const MELEE_JOBS = new Set(['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR']); const MELEE_JOBS = new Set(['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR']);
const CASTER_JOBS = new Set(['BLM', 'SMN', 'RDM', 'PCT']); 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' },
],
};
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) { function guessJob(abilityName, players) {
if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName]; if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName];
const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? ''); const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? '');
@ -448,6 +700,19 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
// ── Merge + Create plan from import ────────────────────────────────────────── // ── 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) { function doImport(data, withMitigations, whereMode, mergeId, newName) {
const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data; const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data;
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations); const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations);
@ -457,6 +722,7 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
return updatePlan(plan.id, { return updatePlan(plan.id, {
mechanics, mechanics,
source: { reportCode, fightName }, source: { reportCode, fightName },
jobComposition: extractJobComp(players),
}); });
} }
@ -476,6 +742,117 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
return updatePlan(mergeId, { mechanics: merged }); 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 = `
<p style="color:var(--t3);font-size:13px;padding:8px 0">
Bitte zuerst die Jobaufstellung konfigurieren.
</p>`;
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` : '';
const icon = MITIG_ICONS[ab.name] ?? '';
const label = localizedAbilityName(ab.name, ab.name);
return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
data-ability="${escHtml(ab.name)}"
data-job="${escHtml(job)}"
data-buff-type="${escHtml(ab.buffType)}"
${title ? `title="${title}"` : ''}
>${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(label)}</button>`;
}).join('');
return `
<div class="ability-job-group">
<div class="ability-job-label">
<span class="aoe-target-job role-${role}">${escHtml(job)}</span>
</div>
<div class="ability-chips">${chips}</div>
</div>`;
}).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, abilityName: localizedAbilityName(abilityName, 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 ────────────────────────────────────────────────────────────── // ── Import Modal ──────────────────────────────────────────────────────────────
let pendingImportData = null; let pendingImportData = null;
@ -591,6 +968,7 @@ window.plannerTab = {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initNewPlanForm(); initNewPlanForm();
initImportModal(); initImportModal();
initAbilityModal();
renderPlanList(); renderPlanList();
renderPlanDetail(null); renderPlanDetail(null);
}); });

View File

@ -80,6 +80,17 @@
</div> </div>
</div> </div>
<!-- Ability Assignment Modal -->
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
<div class="modal-box ability-modal-box">
<div class="modal-title" id="ability-modal-title">Mitigations</div>
<div id="ability-modal-content"></div>
<div class="modal-actions" style="margin-top:16px">
<button id="ability-modal-close" class="btn">Schließen</button>
</div>
</div>
</div>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/tabs.js"></script> <script src="js/tabs.js"></script>
<script src="js/analysis.js"></script> <script src="js/analysis.js"></script>