Planer: DR-Simulation + Schild-Eingabe in Mechanik-Cards

- ABILITY_DR-Map mit DR-Werten pro Ability (Feint = 5% magic)
- simulateDrMultiplier() berechnet multiplikativen DR-Faktor
- Mechanik-Cards zeigen mitigierten Wert (DR-only) + optional mit Schild
- SGE/SCH Schild-Feld (in k) im Info-Panel, wird pro Plan gespeichert
- Farbige Anzeige: grün = unter ∅ HP, rot = darüber

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xziino 2026-05-23 16:43:37 +02:00
parent 8b00d1d2a8
commit e358a4e709
2 changed files with 102 additions and 1 deletions

View File

@ -290,6 +290,11 @@
color: var(--t3); color: var(--t3);
margin-left: 6px; margin-left: 6px;
} }
.mechanic-mitig-row { margin-top: -2px; display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.mechanic-mitig-shield { font-size: 12px; color: var(--t3); }
.mechanic-mitig-val { font-weight: 500; }
.mechanic-mitig--ok { color: var(--green); }
.mechanic-mitig--risk { color: var(--red); }
.mechanic-assignments { .mechanic-assignments {
display: flex; display: flex;
@ -573,6 +578,38 @@
.info-warning--job { color: var(--t2); } .info-warning--job { color: var(--t2); }
.info-warning--missing { color: var(--red); } .info-warning--missing { color: var(--red); }
.info-section--extra { margin-top: 12px; }
.info-extra-row {
display: flex;
align-items: center;
gap: 8px;
}
.info-extra-label {
font-size: 12px;
color: var(--t2);
flex: 1;
}
.info-extra-input-wrap {
display: flex;
align-items: center;
gap: 4px;
}
.info-extra-input {
width: 52px;
font-size: 13px;
padding: 3px 6px;
text-align: right;
}
.info-extra-unit {
font-size: 12px;
color: var(--t3);
}
/* ── Folder Sidebar ──────────────────────────────────────────────────────────── */ /* ── Folder Sidebar ──────────────────────────────────────────────────────────── */
.folder-section { margin-bottom: 2px; } .folder-section { margin-bottom: 2px; }

View File

@ -435,6 +435,15 @@ 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) {
let mult = 1;
for (const a of mechanic.assignments ?? []) {
if (a.buffType === 'shield') continue;
mult *= (1 - (ABILITY_DR[a.ability] ?? 0));
}
return mult;
}
function renderMechanicListHtml(plan) { function renderMechanicListHtml(plan) {
const mechanics = visiblePlanMechanics(plan); const mechanics = visiblePlanMechanics(plan);
if (mechanics.length === 0) { if (mechanics.length === 0) {
@ -483,6 +492,14 @@ function renderMechanicListHtml(plan) {
: badgeHtml; : badgeHtml;
}).join(''); }).join('');
const drOnly = m.unmitigatedDamage ? Math.round(m.unmitigatedDamage * simulateDrMultiplier(m)) : 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 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') : '';
return ` return `
<div class="mechanic-card" data-mechanic-id="${escHtml(m.id)}"> <div class="mechanic-card" data-mechanic-id="${escHtml(m.id)}">
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div> <div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
@ -493,6 +510,10 @@ function renderMechanicListHtml(plan) {
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert${avgHp ? ` <span class="mechanic-avg-hp">∅ ${fmtNumber(avgHp)} HP</span>` : ''}</div>` ? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert${avgHp ? ` <span class="mechanic-avg-hp">∅ ${fmtNumber(avgHp)} HP</span>` : ''}</div>`
: '' : ''
} }
${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row">
${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> mitigiert` : ''}
${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''}
</div>` : ''}
<div class="mechanic-assignments">${assignHtml}</div> <div class="mechanic-assignments">${assignHtml}</div>
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''} ${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
<div class="mechanic-edit-hint">Klicken zum Bearbeiten</div> <div class="mechanic-edit-hint">Klicken zum Bearbeiten</div>
@ -1072,7 +1093,29 @@ function renderInfoPanel(plan) {
warningsHtml += '</div>'; warningsHtml += '</div>';
} }
el.innerHTML = legendHtml + warningsHtml; const extraInfoHtml = `
<div class="info-section info-section--extra">
<div class="info-section-title">Zusätzliche Informationen</div>
<div class="info-extra-row">
<label class="info-extra-label">SGE/SCH Schild</label>
<div class="info-extra-input-wrap">
<input type="text" inputmode="numeric" id="plan-shield-k-input" class="info-extra-input"
value="${plan.shieldK ?? ''}" placeholder="0">
<span class="info-extra-unit">k</span>
</div>
</div>
</div>`;
el.innerHTML = legendHtml + warningsHtml + extraInfoHtml;
const shieldInput = el.querySelector('#plan-shield-k-input');
if (shieldInput) {
shieldInput.addEventListener('input', () => {
const val = parseFloat(shieldInput.value) || 0;
updatePlan(plan.id, { shieldK: val > 0 ? val : null });
refreshMechanicList(plan.id);
});
}
} }
function removeAssignment(planId, mechanicId, abilityName) { function removeAssignment(planId, mechanicId, abilityName) {
@ -1468,6 +1511,27 @@ const MITIG_ICONS = {
'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png', 'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png',
}; };
// DR values (01) for buff/debuff mitigations — shields excluded (no reliable sim).
const ABILITY_DR = {
'Passage of Arms': 0.15,
'Troubadour': 0.15,
'Tactician': 0.15,
'Shield Samba': 0.15,
'Dark Missionary': 0.10,
'Heart of Light': 0.10,
'Temperance': 0.10,
'Sacred Soil': 0.10,
'Expedient': 0.10,
'Collective Unconscious': 0.10,
'Holos': 0.10,
'Kerachole': 0.10,
'Magick Barrier': 0.10,
'Fey Illumination': 0.05,
'Reprisal': 0.10,
'Feint': 0.05, // 10% phys / 5% magic — use magic (conservative)
'Addle': 0.10,
};
// Groups of abilities that are functionally equivalent across different jobs. // Groups of abilities that are functionally equivalent across different jobs.
// Used to suggest replacements when a job is missing from the composition. // Used to suggest replacements when a job is missing from the composition.
const ABILITY_EQUIVALENTS = [ const ABILITY_EQUIVALENTS = [