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);
});
}