diff --git a/css/planner.css b/css/planner.css index 0e9f7ab..c590c8b 100644 --- a/css/planner.css +++ b/css/planner.css @@ -707,6 +707,39 @@ .timeline-hint { font-size: 12px; color: var(--t3); + margin-bottom: 8px; +} + +.timeline-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.timeline-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: var(--r); + background: rgba(255,255,255,.025); + color: var(--t2); + font-size: 12px; + cursor: pointer; + user-select: none; +} + +.timeline-toggle:hover { + border-color: var(--borderem); + color: var(--t1); +} + +.timeline-toggle input { + width: auto !important; + min-width: 0 !important; + margin: 0; } .timeline-empty, diff --git a/js/planner.js b/js/planner.js index d2b0c58..73e3734 100644 --- a/js/planner.js +++ b/js/planner.js @@ -41,6 +41,7 @@ function createPlan(name) { updatedAt: Date.now(), source: null, mitigationNames: {}, + timelineOptions: { includeShields: false, includePersonal: false }, folderId: null, jobComposition: Array(8).fill(''), mechanics: [] @@ -392,8 +393,18 @@ function renderPlanDetail(plan) {
Zeitstrahl
-
Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten
+
+ + +
+
Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten
${renderTimelineHtml(plan)}
@@ -419,6 +430,7 @@ function renderPlanDetail(plan) { document.getElementById('name-import-open-btn')?.addEventListener('click', () => { showNameImportModal(plan.id); }); + initTimelineOptions(plan.id); initTimeline(plan.id); initMechanicClicks(plan.id); renderInfoPanel(plan); @@ -586,6 +598,25 @@ function initJobSlots(planId) { }); } +function initTimelineOptions(planId) { + const shields = document.getElementById('timeline-include-shields'); + const personal = document.getElementById('timeline-include-personal'); + const update = () => { + const plan = getPlan(planId); + if (!plan) return; + updatePlan(planId, { + timelineOptions: { + ...(plan.timelineOptions ?? {}), + includeShields: !!shields?.checked, + includePersonal: !!personal?.checked, + }, + }); + refreshMechanicList(planId); + }; + shields?.addEventListener('change', update); + personal?.addEventListener('change', update); +} + // ── Timeline ───────────────────────────────────────────────────────────────── function planDurationMs(plan) { @@ -609,9 +640,39 @@ const JOB_GANTT_ORDER = { 'PLD': 40, 'WAR': 41, 'DRK': 42, 'GNB': 43, }; +const TIMELINE_PERSONAL_ABILITIES = new Set([ + 'Guardian', + 'Bloodwhetting', + 'Divine Benison', + 'Intersection', + 'the Spire', + 'Haima', + 'Eukrasian Diagnosis', + 'Differential Diagnosis', + 'Seraphic Veil', + 'Radiant Aegis', + 'Tempera Coat', +]); + +function timelineOptions(plan) { + return { + includeShields: !!plan?.timelineOptions?.includeShields, + includePersonal: !!plan?.timelineOptions?.includePersonal, + }; +} + +function timelineAbilityVisible(ability, options) { + const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(ability.name); + const isShield = ability.buffType === 'shield'; + if (isPersonal && !options.includePersonal) return false; + if (isShield && !options.includeShields && ability.name !== 'Panhaima') return false; + return true; +} + function timelinePlayerRows(plan) { const roster = plan.playerRoster ?? []; const rows = []; + const options = timelineOptions(plan); const jobEntries = (plan.jobComposition ?? []) .map((job, idx) => ({ job, idx })) .filter(e => !!e.job) @@ -620,7 +681,7 @@ function timelinePlayerRows(plan) { const name = roster[idx]?.name ?? ''; const role = JOB_ROLE[job] ?? ''; const abilities = (JOB_ABILITIES[job] ?? []) - .filter(ab => ab.buffType !== 'shield' || ab.name === 'Panhaima'); + .filter(ab => timelineAbilityVisible(ab, options)); abilities.forEach((ab, abilityIdx) => { rows.push({ idx, job, ability: ab.name, buffType: ab.buffType, name, role, firstForJob: abilityIdx === 0 }); }); @@ -1305,7 +1366,16 @@ async function showMissingBossActionsMenu(planId, x, y) { return; } - const isGenericAttack = event => String(event?.abilityName ?? '').trim().toLowerCase() === 'attack'; + const genericAttackNames = new Set(['attack', 'attacke', 'auto attack', 'auto-attack', 'angriff', 'attaque', '攻撃']); + const isGenericAttack = event => { + const name = String(event?.abilityName ?? '') + .trim() + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + const id = parseInt(event?.abilityId ?? 0, 10) || 0; + return id > 0 && id <= 7 || genericAttackNames.has(name); + }; const missing = (json.boss_events ?? json.aoe_events ?? []) .filter(event => !isGenericAttack(event)) .filter(event => !mechanicExistsInPlan(plan, event)) @@ -1313,8 +1383,15 @@ async function showMissingBossActionsMenu(planId, x, y) { const sourceStart = plan.source?.fightStart ?? json.fight_start ?? 0; const byAttack = new Map(); + const idsByNameType = new Map(); for (const event of missing) { - const key = `${event.abilityId || event.abilityName}::${event.abilityName}`; + const nameKey = String(event.abilityName ?? '').trim().toLowerCase(); + const typeKey = event.isHeavyTankbuster ? 'tankbuster' : 'aoe'; + const idKey = String(event.abilityId ?? ''); + const nameTypeKey = `${nameKey}::${typeKey}`; + if (!idsByNameType.has(nameTypeKey)) idsByNameType.set(nameTypeKey, new Set()); + if (idKey) idsByNameType.get(nameTypeKey).add(idKey); + const key = `${idKey || nameKey}::${nameTypeKey}`; if (!byAttack.has(key)) byAttack.set(key, []); byAttack.get(key).push(event); } @@ -1325,8 +1402,11 @@ async function showMissingBossActionsMenu(planId, x, y) { for (const events of [...byAttack.values()].sort((a, b) => a[0].abilityName.localeCompare(b[0].abilityName))) { const first = events[0]; const count = events.length > 1 ? ` (${events.length}x)` : ''; + const nameTypeKey = `${String(first.abilityName ?? '').trim().toLowerCase()}::${first.isHeavyTankbuster ? 'tankbuster' : 'aoe'}`; + const duplicateName = (idsByNameType.get(nameTypeKey)?.size ?? 0) > 1; + const idLabel = duplicateName && first.abilityId ? ` [${first.abilityId}]` : ''; attackItems.push({ - label: `${first.abilityName}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`, + label: `${first.abilityName}${idLabel}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`, onClick: () => addMechanicsFromEvents(planId, events, json), }); }