adjustments to skill behaviour

This commit is contained in:
Akurosia Kamo 2026-05-23 21:00:19 +02:00
parent 4ec929ebb7
commit d0f54049e6

View File

@ -435,15 +435,35 @@ function avgNonTankMaxHp(plan) {
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length); return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length);
} }
function simulateDrMultiplier(mechanic) { function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) {
let mult = 1; let mult = 1;
for (const a of mechanic.assignments ?? []) { for (const a of assignments) {
if (a.buffType === 'shield') continue; if (a.buffType === 'shield') continue;
mult *= (1 - (ABILITY_DR[a.ability] ?? 0)); mult *= (1 - (ABILITY_DR[a.ability] ?? 0));
} }
return mult; 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) { function renderMechanicListHtml(plan) {
const mechanics = visiblePlanMechanics(plan); const mechanics = visiblePlanMechanics(plan);
if (mechanics.length === 0) { if (mechanics.length === 0) {
@ -462,7 +482,8 @@ function renderMechanicListHtml(plan) {
const avgHp = avgNonTankMaxHp(plan); const avgHp = avgNonTankMaxHp(plan);
return mechanics.map(m => { return mechanics.map(m => {
const sorted = sortedAssignments(m.assignments); const effective = effectiveAssignmentsForMechanic(plan, m);
const sorted = sortedAssignments(effective);
const assignHtml = sorted.length === 0 const assignHtml = sorted.length === 0
? '<span class="mechanic-no-assign">Keine Zuweisung</span>' ? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
: sorted.map(a => { : sorted.map(a => {
@ -478,7 +499,7 @@ function renderMechanicListHtml(plan) {
const badgeHtml = `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}> const badgeHtml = `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}>
${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''} ${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}
${label} ${label}
<button class="badge-remove" data-mechanic-id="${escHtml(m.id)}" data-ability="${escHtml(a.ability)}" title="Entfernen">×</button> <button class="badge-remove" data-mechanic-id="${escHtml(a.sourceMechanicId ?? m.id)}" data-ability="${escHtml(a.ability)}" data-job="${escHtml(a.job ?? '')}" title="Entfernen">×</button>
</span>`; </span>`;
const hintHtml = suggestions.map(s => const hintHtml = suggestions.map(s =>
`<span class="badge-equiv-hint">→ ${escHtml(s.ability)} (${escHtml(s.job)})?</span>` `<span class="badge-equiv-hint">→ ${escHtml(s.ability)} (${escHtml(s.job)})?</span>`
@ -492,10 +513,10 @@ function renderMechanicListHtml(plan) {
: badgeHtml; : badgeHtml;
}).join(''); }).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 shieldVal = (plan.shieldK ?? 0) * 1000;
const mitigFull = Math.max(0, drOnly - shieldVal); 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 hasShield = shieldVal > 0;
const drOnlyCls = avgHp ? (drOnly <= avgHp ? 'mechanic-mitig--ok' : 'mechanic-mitig--risk') : ''; const drOnlyCls = avgHp ? (drOnly <= avgHp ? 'mechanic-mitig--ok' : 'mechanic-mitig--risk') : '';
const fullCls = avgHp ? (mitigFull <= 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; 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) { function findNearestMechanic(plan, timestamp) {
const mechanics = visiblePlanMechanics(plan); const mechanics = visiblePlanMechanics(plan);
if (!mechanics.length) return null; if (!mechanics.length) return null;
@ -714,13 +762,17 @@ function renderTimelineHtml(plan) {
const playerRows = rows.map(row => { const playerRows = rows.map(row => {
const blocks = []; const blocks = [];
for (const m of mechanics) { const rowAssignments = canonicalAssignmentActivations(plan).filter(entry => {
for (const a of sortedAssignments(m.assignments ?? [])) { const assignment = entry.assignment;
if (a.ability !== row.ability) continue; if (assignment.ability !== row.ability) return false;
const assignedJob = a.job ?? ''; const assignedJob = assignment.job ?? '';
if (assignedJob && assignedJob !== row.job) continue; return !assignedJob || assignedJob === row.job;
const start = Number.isFinite(Number(a.timestamp)) ? Number(a.timestamp) : m.timestamp; });
const durationSec = assignmentDurationSeconds(a); 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 cooldownSec = assignmentCooldownSeconds(a);
const left = Math.max(0, Math.min(100, (start / duration) * 100)); 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)); const widthPct = Math.max(1.2, Math.min(100 - left, (durationSec * 1000 / duration) * 100));
@ -747,7 +799,6 @@ function renderTimelineHtml(plan) {
</button> </button>
`); `);
} }
}
const icon = MITIG_ICONS[row.ability] ?? ''; const icon = MITIG_ICONS[row.ability] ?? '';
const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability; const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability;
const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : ''; const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : '';
@ -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 === 'durationSeconds') assignment.durationSeconds = Math.max(1, Math.round(Number(value)));
if (field === 'cooldownSeconds') assignment.cooldownSeconds = Math.max(0, Math.round(Number(value))); if (field === 'cooldownSeconds') assignment.cooldownSeconds = Math.max(0, Math.round(Number(value)));
updatePlan(planId, { mechanics: plan.mechanics }); updatePlan(planId, { mechanics: plan.mechanics });
refreshTimeline(planId); refreshMechanicList(planId);
} }
function setTimelineAssignmentJob(planId, mechanicId, ability, job, nextJob) { 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); const plan = getPlan(planId);
if (!plan) return; if (!plan) return;
const mechanic = plan.mechanics.find(m => m.id === mechanicId); const mechanic = plan.mechanics.find(m => m.id === mechanicId);
if (!mechanic) return; 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 }); updatePlan(planId, { mechanics: plan.mechanics });
refreshMechanicList(planId); refreshMechanicList(planId);
if (abilityModalMechanicId === mechanicId) renderAbilityModalContent(); if (abilityModalMechanicId === mechanicId) renderAbilityModalContent();
@ -1160,7 +1213,7 @@ function initMechanicClicks(planId) {
const removeBtn = e.target.closest('.badge-remove'); const removeBtn = e.target.closest('.badge-remove');
if (removeBtn) { if (removeBtn) {
e.stopPropagation(); e.stopPropagation();
removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability); removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability, removeBtn.dataset.job ?? null);
return; return;
} }
const deleteBtn = e.target.closest('.mechanic-delete-btn'); const deleteBtn = e.target.closest('.mechanic-delete-btn');
@ -1179,7 +1232,7 @@ function initMechanicClicks(planId) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const removeBtn = badge.querySelector('.badge-remove'); 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);
}); });
} }