forked from xziino/ff14-mitigator
Planner: assignment sorting, icons, remove buttons, missing-job highlight, mechanic delete, job pre-fill on import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8f29619ef5
commit
969484a1dc
@ -162,7 +162,7 @@
|
|||||||
/* ── 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 6px;
|
padding: 12px 6px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
@ -175,6 +175,13 @@
|
|||||||
.mechanic-card:last-child { border-bottom: none; }
|
.mechanic-card:last-child { border-bottom: none; }
|
||||||
.mechanic-card:hover { background: var(--bg2); }
|
.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 {
|
.mechanic-edit-hint {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--t3);
|
color: var(--t3);
|
||||||
@ -229,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);
|
||||||
@ -360,6 +397,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ability-chip {
|
.ability-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid var(--borderem);
|
border: 1px solid var(--borderem);
|
||||||
border-radius: var(--r);
|
border-radius: var(--r);
|
||||||
|
|||||||
164
js/planner.js
164
js/planner.js
@ -210,34 +210,44 @@ function renderMechanicListHtml(plan) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan.mechanics.map(m => `
|
const activeJobSet = new Set(plan.jobComposition.filter(j => j));
|
||||||
<div class="mechanic-card" data-mechanic-id="${escHtml(m.id)}">
|
|
||||||
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
|
return plan.mechanics.map(m => {
|
||||||
<div class="mechanic-body">
|
const sorted = sortedAssignments(m.assignments);
|
||||||
${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''}
|
const assignHtml = sorted.length === 0
|
||||||
<div class="mechanic-name">${escHtml(m.name)}</div>
|
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
||||||
${m.unmitigatedDamage
|
: sorted.map(a => {
|
||||||
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert</div>`
|
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||||
: ''
|
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||||
}
|
: 'badge-assign-buff';
|
||||||
<div class="mechanic-assignments">
|
const isMissing = !!a.job && !activeJobSet.has(a.job);
|
||||||
${m.assignments.length === 0
|
const icon = MITIG_ICONS[a.ability] ?? '';
|
||||||
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
const label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability);
|
||||||
: m.assignments.map(a => {
|
const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : '';
|
||||||
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
return `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}>
|
||||||
: a.buffType === 'shield' ? 'badge-assign-shield'
|
${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}
|
||||||
: a.buffType === 'buff' ? 'badge-assign-buff'
|
${label}
|
||||||
: '';
|
<button class="badge-remove" data-mechanic-id="${escHtml(m.id)}" data-ability="${escHtml(a.ability)}" title="Entfernen">×</button>
|
||||||
const label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability);
|
</span>`;
|
||||||
return `<span class="badge badge-assign ${cls}">${label}</span>`;
|
}).join('');
|
||||||
}).join('')
|
|
||||||
|
return `
|
||||||
|
<div class="mechanic-card" data-mechanic-id="${escHtml(m.id)}">
|
||||||
|
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
|
||||||
|
<div class="mechanic-body">
|
||||||
|
${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''}
|
||||||
|
<div class="mechanic-name">${escHtml(m.name)}</div>
|
||||||
|
${m.unmitigatedDamage
|
||||||
|
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert</div>`
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
|
<div class="mechanic-assignments">${assignHtml}</div>
|
||||||
|
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
|
||||||
|
<div class="mechanic-edit-hint">Klicken zum Bearbeiten</div>
|
||||||
</div>
|
</div>
|
||||||
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
|
<button class="mechanic-delete-btn plan-btn plan-btn-danger" data-mechanic-id="${escHtml(m.id)}" title="Mechanik löschen">✕</button>
|
||||||
<div class="mechanic-edit-hint">Klicken zum Bearbeiten</div>
|
</div>`;
|
||||||
</div>
|
}).join('');
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
||||||
@ -276,6 +286,7 @@ function initJobSlots(planId) {
|
|||||||
const role = JOB_ROLE[sel.value] ?? '';
|
const role = JOB_ROLE[sel.value] ?? '';
|
||||||
slot.className = 'job-slot' + (role ? ` job-slot--${role}` : '');
|
slot.className = 'job-slot' + (role ? ` job-slot--${role}` : '');
|
||||||
}
|
}
|
||||||
|
refreshMechanicList(planId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,10 +299,42 @@ function refreshMechanicList(planId) {
|
|||||||
if (el) el.innerHTML = renderMechanicListHtml(plan);
|
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) {
|
function initMechanicClicks(planId) {
|
||||||
const list = document.getElementById('mechanic-list');
|
const list = document.getElementById('mechanic-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
list.addEventListener('click', e => {
|
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');
|
const card = e.target.closest('.mechanic-card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
showAbilityModal(planId, card.dataset.mechanicId);
|
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) {
|
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] ?? '');
|
||||||
@ -569,6 +662,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);
|
||||||
@ -577,7 +683,8 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
|||||||
const plan = createPlan(newName || fightName || 'Importierter Plan');
|
const plan = createPlan(newName || fightName || 'Importierter Plan');
|
||||||
return updatePlan(plan.id, {
|
return updatePlan(plan.id, {
|
||||||
mechanics,
|
mechanics,
|
||||||
source: { reportCode, fightName },
|
source: { reportCode, fightName },
|
||||||
|
jobComposition: extractJobComp(players),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,12 +756,13 @@ function renderAbilityModalContent() {
|
|||||||
const activeClass = isActive ? ' ability-chip--active' : '';
|
const activeClass = isActive ? ' ability-chip--active' : '';
|
||||||
const otherClass = byOtherJob ? ' ability-chip--other-job' : '';
|
const otherClass = byOtherJob ? ' ability-chip--other-job' : '';
|
||||||
const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : '';
|
const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : '';
|
||||||
|
const icon = MITIG_ICONS[ab.name] ?? '';
|
||||||
return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
|
return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
|
||||||
data-ability="${escHtml(ab.name)}"
|
data-ability="${escHtml(ab.name)}"
|
||||||
data-job="${escHtml(job)}"
|
data-job="${escHtml(job)}"
|
||||||
data-buff-type="${escHtml(ab.buffType)}"
|
data-buff-type="${escHtml(ab.buffType)}"
|
||||||
${title ? `title="${title}"` : ''}
|
${title ? `title="${title}"` : ''}
|
||||||
>${escHtml(ab.name)}</button>`;
|
>${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(ab.name)}</button>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user