diff --git a/js/planner.js b/js/planner.js index 29833ef..d6f283a 100644 --- a/js/planner.js +++ b/js/planner.js @@ -435,15 +435,35 @@ function avgNonTankMaxHp(plan) { return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length); } -function simulateDrMultiplier(mechanic) { +function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) { let mult = 1; - for (const a of mechanic.assignments ?? []) { + for (const a of assignments) { if (a.buffType === 'shield') continue; mult *= (1 - (ABILITY_DR[a.ability] ?? 0)); } return mult; } +function effectiveAssignmentsForMechanic(plan, targetMechanic) { + const result = []; + const seen = new Set(); + + for (const entry of canonicalAssignmentActivations(plan)) { + if (targetMechanic.timestamp < entry.start || targetMechanic.timestamp > entry.end) continue; + const assignment = entry.assignment; + const key = `${assignment.ability}::${assignment.job ?? ''}`; + if (seen.has(key)) continue; + seen.add(key); + result.push({ + ...assignment, + sourceMechanicId: entry.mechanic.id, + sourceStart: entry.start, + }); + } + + return result; +} + function renderMechanicListHtml(plan) { const mechanics = visiblePlanMechanics(plan); if (mechanics.length === 0) { @@ -462,7 +482,8 @@ function renderMechanicListHtml(plan) { const avgHp = avgNonTankMaxHp(plan); return mechanics.map(m => { - const sorted = sortedAssignments(m.assignments); + const effective = effectiveAssignmentsForMechanic(plan, m); + const sorted = sortedAssignments(effective); const assignHtml = sorted.length === 0 ? 'Keine Zuweisung' : sorted.map(a => { @@ -478,7 +499,7 @@ function renderMechanicListHtml(plan) { const badgeHtml = ` ${icon ? `` : ''} ${label} - + `; const hintHtml = suggestions.map(s => `→ ${escHtml(s.ability)} (${escHtml(s.job)})?` @@ -492,10 +513,10 @@ function renderMechanicListHtml(plan) { : badgeHtml; }).join(''); - const drOnly = m.unmitigatedDamage ? Math.round(m.unmitigatedDamage * simulateDrMultiplier(m)) : 0; + const drOnly = m.unmitigatedDamage ? Math.round(m.unmitigatedDamage * simulateDrMultiplier(m, effective)) : 0; const shieldVal = (plan.shieldK ?? 0) * 1000; const mitigFull = Math.max(0, drOnly - shieldVal); - const hasDrAssign = (m.assignments ?? []).some(a => a.buffType !== 'shield' && (ABILITY_DR[a.ability] ?? 0) > 0); + const hasDrAssign = effective.some(a => a.buffType !== 'shield' && (ABILITY_DR[a.ability] ?? 0) > 0); const hasShield = shieldVal > 0; const drOnlyCls = avgHp ? (drOnly <= avgHp ? 'mechanic-mitig--ok' : 'mechanic-mitig--risk') : ''; const fullCls = avgHp ? (mitigFull <= avgHp ? 'mechanic-mitig--ok' : 'mechanic-mitig--risk') : ''; @@ -620,6 +641,33 @@ function assignmentStartMs(mechanic, assignment) { return Number.isFinite(Number(assignment?.timestamp)) ? Number(assignment.timestamp) : mechanic.timestamp; } +function canonicalAssignmentActivations(plan) { + const entries = []; + for (const mechanic of visiblePlanMechanics(plan)) { + for (const assignment of mechanic.assignments ?? []) { + const start = assignmentStartMs(mechanic, assignment); + const durationSec = assignmentDurationSeconds(assignment); + entries.push({ + mechanic, + assignment, + start, + durationSec, + end: start + durationSec * 1000, + }); + } + } + + entries.sort((a, b) => a.start - b.start); + const activeUntilBySkill = new Map(); + return entries.filter(entry => { + const key = `${entry.assignment.ability}::${entry.assignment.job ?? ''}`; + const activeUntil = activeUntilBySkill.get(key) ?? -Infinity; + if (entry.start < activeUntil) return false; + activeUntilBySkill.set(key, entry.end); + return true; + }); +} + function findNearestMechanic(plan, timestamp) { const mechanics = visiblePlanMechanics(plan); if (!mechanics.length) return null; @@ -714,13 +762,17 @@ function renderTimelineHtml(plan) { const playerRows = rows.map(row => { const blocks = []; - for (const m of mechanics) { - for (const a of sortedAssignments(m.assignments ?? [])) { - if (a.ability !== row.ability) continue; - const assignedJob = a.job ?? ''; - if (assignedJob && assignedJob !== row.job) continue; - const start = Number.isFinite(Number(a.timestamp)) ? Number(a.timestamp) : m.timestamp; - const durationSec = assignmentDurationSeconds(a); + const rowAssignments = canonicalAssignmentActivations(plan).filter(entry => { + const assignment = entry.assignment; + if (assignment.ability !== row.ability) return false; + const assignedJob = assignment.job ?? ''; + return !assignedJob || assignedJob === row.job; + }); + for (const item of rowAssignments) { + const m = item.mechanic; + const a = item.assignment; + const start = item.start; + const durationSec = item.durationSec; const cooldownSec = assignmentCooldownSeconds(a); const left = Math.max(0, Math.min(100, (start / duration) * 100)); const widthPct = Math.max(1.2, Math.min(100 - left, (durationSec * 1000 / duration) * 100)); @@ -746,7 +798,6 @@ function renderTimelineHtml(plan) { ${escHtml(abilityLabel)} `); - } } const icon = MITIG_ICONS[row.ability] ?? ''; const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability; @@ -838,7 +889,7 @@ function setTimelineAssignmentField(planId, mechanicId, ability, job, field, val if (field === 'durationSeconds') assignment.durationSeconds = Math.max(1, Math.round(Number(value))); if (field === 'cooldownSeconds') assignment.cooldownSeconds = Math.max(0, Math.round(Number(value))); updatePlan(planId, { mechanics: plan.mechanics }); - refreshTimeline(planId); + refreshMechanicList(planId); } function setTimelineAssignmentJob(planId, mechanicId, ability, job, nextJob) { @@ -1133,12 +1184,14 @@ function renderInfoPanel(plan) { } } -function removeAssignment(planId, mechanicId, abilityName) { +function removeAssignment(planId, mechanicId, abilityName, job = null) { 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); + mechanic.assignments = mechanic.assignments.filter(a => + a.ability !== abilityName || (job !== null && (a.job ?? '') !== job) + ); updatePlan(planId, { mechanics: plan.mechanics }); refreshMechanicList(planId); if (abilityModalMechanicId === mechanicId) renderAbilityModalContent(); @@ -1160,7 +1213,7 @@ function initMechanicClicks(planId) { const removeBtn = e.target.closest('.badge-remove'); if (removeBtn) { e.stopPropagation(); - removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability); + removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability, removeBtn.dataset.job ?? null); return; } const deleteBtn = e.target.closest('.mechanic-delete-btn'); @@ -1179,7 +1232,7 @@ function initMechanicClicks(planId) { e.preventDefault(); e.stopPropagation(); const removeBtn = badge.querySelector('.badge-remove'); - if (removeBtn) removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability); + if (removeBtn) removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability, removeBtn.dataset.job ?? null); }); }