feat: add source-owned combat mechanic programs

This commit is contained in:
JB
2026-07-27 13:35:00 -04:00
parent 5a67b252aa
commit 4eb754e7c8
62 changed files with 14815 additions and 905 deletions
@@ -0,0 +1,57 @@
<script lang="ts">
import type { SkillCalculation } from '$lib/calculations/skill';
let {
result,
abilityNames = {}
}: {
result: SkillCalculation;
abilityNames?: Record<string, string>;
} = $props();
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
</script>
<article class:incomplete={result.status === 'INSUFFICIENT_DATA'} class="skill-calculation" aria-live="polite">
<header>
<div><p>SKILL OUTPUT</p><h3>Specialization abilities</h3></div>
<b>{result.status === 'OK' ? 'CALCULATED' : 'INSUFFICIENT DATA'}</b>
</header>
{#if result.status === 'INSUFFICIENT_DATA'}
<div class="missing">
<strong>Skill calculation unavailable</strong>
<span>ISAC requires the following data:</span>
<ul>{#each result.missing as item}<li>{item.replaceAll(':', ' · ')}</li>{/each}</ul>
</div>
{:else}
<div class="combined">
<span><small>SINGLE-TARGET DPS</small><b>{number.format(result.combined.singleTargetDps)}</b></span>
<span><small>ALL-TARGET DPS</small><b>{number.format(result.combined.aggregateDps)}</b></span>
<span><small>ENCOUNTER DAMAGE</small><b>{number.format(result.combined.aggregateEncounterDamage)}</b></span>
</div>
<div class="ability-results">
{#each result.abilities as ability}
<div class:disabled={!ability.enabled}>
<strong>{abilityNames[ability.abilityId] ?? ability.abilityId}</strong>
<span>{ability.enabled ? `${ability.activationCount} ACTIVATIONS` : 'OFF'}</span>
<small>{number.format(ability.damage.singleTargetDps)} SINGLE DPS</small>
<small>{number.format(ability.damage.aggregateDps)} TOTAL DPS</small>
</div>
{/each}
</div>
{/if}
<footer>SKILLS CAN CRIT AND MULTI-SHOT · SKILLS DO NOT HEADSHOT · ENCOUNTER TRIGGERS FOLLOW THE PRIMARY WEAPON TIMELINE</footer>
</article>
<style>
.skill-calculation{display:grid;gap:var(--shd-gap-md);padding:var(--shd-gap-md);border:1px solid var(--shd-border);border-top:2px solid var(--shd-info);background:var(--shd-surface)}
.skill-calculation.incomplete{border-top-color:var(--shd-danger)}
header{display:flex;justify-content:space-between;align-items:start;gap:var(--shd-gap-md)}
header p,h3{margin:0}header p{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1.1px}h3{text-transform:uppercase;font-size:var(--shd-type-body);letter-spacing:.7px}
header>b{padding:5px 7px;border:1px solid var(--shd-info);color:var(--shd-info);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.incomplete header>b{border-color:var(--shd-danger);color:var(--shd-danger)}
.combined{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--shd-border)}.combined span{display:grid;gap:4px;padding:9px;border-left:1px solid var(--shd-border)}.combined span:first-child{border-left:0}.combined b{color:var(--shd-info);font-size:20px}
small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
.ability-results{display:grid;border:1px solid var(--shd-border)}.ability-results>div{display:grid;grid-template-columns:minmax(140px,1fr) repeat(3,auto);gap:var(--shd-gap-sm);padding:7px 8px;border-top:1px solid var(--shd-border);align-items:center}.ability-results>div:first-child{border-top:0}.ability-results>div.disabled{opacity:.55}.ability-results span{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:900}
.missing{display:grid;gap:4px;padding:10px;border:1px solid var(--shd-danger);border-left-width:2px;background:#1d1212;color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.missing strong{color:var(--shd-danger);text-transform:uppercase}.missing ul{margin:2px 0 0;padding-left:18px}
footer{color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
@media(max-width:700px){header,.combined{display:grid;grid-template-columns:1fr}.combined span{border-left:0;border-top:1px solid var(--shd-border)}.combined span:first-child{border-top:0}.ability-results>div{grid-template-columns:1fr 1fr}.ability-results strong{grid-column:1/-1}}
</style>