91 lines
7.6 KiB
Svelte
91 lines
7.6 KiB
Svelte
<script lang="ts">
|
|
export type WeaponCalculationResult = {
|
|
weaponName: string;
|
|
weaponLabel: string;
|
|
status: 'complete' | 'insufficient';
|
|
bodyHit?: number;
|
|
criticalBodyHit?: number;
|
|
headshot?: number;
|
|
criticalHeadshot?: number;
|
|
burstDps?: number;
|
|
sustainedDps?: number;
|
|
encounterDps?: number;
|
|
effectSummaries?: Array<{
|
|
id: string;
|
|
name: string;
|
|
mode: 'off' | 'peak' | 'encounter';
|
|
averageStacks: number;
|
|
maxStacksReached: number;
|
|
maxStacks: number;
|
|
stacksAtEnd: number;
|
|
buildUpSeconds: number | null;
|
|
maintained: boolean;
|
|
uptimePercent?: number;
|
|
activationCount?: number;
|
|
instantTotal?: number;
|
|
}>;
|
|
missing?: string[];
|
|
revisionLabel?: string;
|
|
};
|
|
|
|
let {
|
|
result,
|
|
resolverHint = 'Complete the required build configuration.'
|
|
}: {
|
|
result: WeaponCalculationResult;
|
|
resolverHint?: string;
|
|
} = $props();
|
|
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
|
|
function format(value: number | undefined) { return value === undefined ? '—' : number.format(value); }
|
|
const loadoutRequirement = (item: string) =>
|
|
/weapon$| gear$|OS Protocol|Specialization focus|focus abilities|Skill Chips?$/i.test(item);
|
|
const systemRequirements = $derived((result.missing ?? []).filter((item) => !loadoutRequirement(item)));
|
|
const loadoutRequirementCount = $derived((result.missing ?? []).length - systemRequirements.length);
|
|
</script>
|
|
|
|
<article class:incomplete={result.status === 'insufficient'} class="weapon-calculation" aria-labelledby={`${result.weaponLabel}-calculation`} aria-live="polite">
|
|
<header>
|
|
<div><p>{result.weaponLabel}</p><h3 id={`${result.weaponLabel}-calculation`}>{result.weaponName}</h3></div>
|
|
<b>{result.status === 'complete' ? 'CALCULATED' : 'INSUFFICIENT DATA'}</b>
|
|
</header>
|
|
{#if result.status === 'insufficient'}
|
|
<div class="missing" role="status">
|
|
<strong>OUTPUT BLOCKED</strong>
|
|
<span>{systemRequirements[0] ?? `${loadoutRequirementCount} loadout requirement${loadoutRequirementCount === 1 ? '' : 's'}`}</span>
|
|
<small>{loadoutRequirementCount ? `${loadoutRequirementCount} loadout requirement${loadoutRequirementCount === 1 ? '' : 's'} remain. ${resolverHint}` : resolverHint}</small>
|
|
</div>
|
|
{:else}
|
|
<div class="damage-grid" aria-label={`${result.weaponName} damage estimates`}>
|
|
<span><small>BODY HIT</small><b>{format(result.bodyHit)}</b></span><span><small>CRITICAL BODY</small><b>{format(result.criticalBodyHit)}</b></span>
|
|
<span><small>HEADSHOT</small><b>{format(result.headshot)}</b></span><span><small>CRITICAL HEADSHOT</small><b>{format(result.criticalHeadshot)}</b></span>
|
|
</div>
|
|
<div class="dps-grid"><span><small>BURST DPS</small><b>{format(result.burstDps)}</b></span><span><small>SUSTAINED DPS</small><b>{format(result.sustainedDps)}</b></span><span><small>ENCOUNTER DPS</small><b>{format(result.encounterDps)}</b></span></div>
|
|
{#if result.effectSummaries?.length}
|
|
<div class="stack-telemetry">
|
|
<header><span>CONDITIONAL STACK TELEMETRY</span><b>{result.effectSummaries.length} EFFECTS</b></header>
|
|
{#each result.effectSummaries as effect}
|
|
<div>
|
|
<strong>{effect.name}</strong>
|
|
<span>{effect.mode.toUpperCase()}</span>
|
|
<small>AVG {effect.averageStacks.toFixed(1)} / {effect.maxStacks}</small>
|
|
<small>PEAK {effect.maxStacksReached} · END {effect.stacksAtEnd}</small>
|
|
<small>{(effect.uptimePercent ?? 0).toFixed(0)}% UPTIME · {effect.activationCount ?? 0} PROCS</small>
|
|
<em>{effect.mode === 'off' ? 'EXCLUDED' : effect.buildUpSeconds === null ? 'MAX NOT REACHED' : `MAX IN ${effect.buildUpSeconds.toFixed(1)}s`}</em>
|
|
<b class:maintained={effect.maintained}>{effect.maintained ? 'MAINTAINED' : 'NOT MAINTAINED'}</b>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{#if result.status === 'complete'}<footer><span>MODELED, BEFORE ENEMY MITIGATION</span>{#if result.revisionLabel}<span>{result.revisionLabel}</span>{/if}</footer>{/if}
|
|
</article>
|
|
|
|
<style>
|
|
.weapon-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-orange);background:var(--shd-surface)}
|
|
.weapon-calculation.incomplete{gap:6px;padding:8px;border-top-color:var(--shd-danger)}header{display:flex;justify-content:space-between;gap:var(--shd-gap-md);align-items:start}header p{margin:0 0 3px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1.1px}h3{margin:0;font-size:var(--shd-type-body);letter-spacing:.7px;text-transform:uppercase}header>b{padding:5px 7px;border:1px solid #74552a;background:#1d180e;color:var(--shd-orange);font-size:var(--shd-type-telemetry);letter-spacing:.8px;white-space:nowrap}.incomplete header h3{font-size:10px}.incomplete header>b{padding:3px 5px;border-color:#563734;background:#1d1212;color:var(--shd-danger);font-size:8px}
|
|
.damage-grid,.dps-grid{display:grid;border:1px solid var(--shd-border)}.damage-grid{grid-template-columns:repeat(2,1fr)}.dps-grid{grid-template-columns:repeat(3,1fr)}.damage-grid span,.dps-grid span{display:grid;gap:4px;padding:9px;border-left:1px solid var(--shd-border);border-top:1px solid var(--shd-border)}.damage-grid span:nth-child(odd),.dps-grid span:first-child{border-left:0}.damage-grid span:nth-child(-n+2){border-top:0}.dps-grid b{color:var(--shd-orange);font-size:20px}.damage-grid b{font-size:var(--shd-type-body)}small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.missing{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:3px 8px;padding:7px 8px;border:1px solid #563734;border-left:2px solid var(--shd-danger);background:#171111;color:var(--shd-text-muted)}.missing strong{color:var(--shd-danger);font-size:9px;letter-spacing:.8px;white-space:nowrap}.missing>span{overflow:hidden;color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:800;text-overflow:ellipsis;white-space:nowrap}.missing>small{grid-column:1/-1;color:var(--shd-text-dim);font-size:9px;font-weight:700;letter-spacing:.35px}footer{display:flex;justify-content:space-between;gap:var(--shd-gap-sm);color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
|
|
.stack-telemetry{display:grid;border:1px solid var(--shd-model-border);background:var(--shd-model-surface)}.stack-telemetry>header{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-bottom:1px solid var(--shd-model-border);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.stack-telemetry>header span{color:var(--shd-info)}.stack-telemetry>header b{color:var(--shd-text-muted)}.stack-telemetry>div{display:grid;grid-template-columns:minmax(120px,1.2fr) .45fr .7fr .8fr .9fr .85fr .8fr;gap:7px;align-items:center;padding:7px 8px;border-top:1px solid var(--shd-border-soft);font-size:var(--shd-type-telemetry)}.stack-telemetry>div:first-of-type{border-top:0}.stack-telemetry>div>span,.stack-telemetry>div>small,.stack-telemetry>div>em,.stack-telemetry>div>b{font-size:inherit;font-style:normal;letter-spacing:.5px}.stack-telemetry>div>span{color:var(--shd-orange);font-weight:900}.stack-telemetry>div>em{color:var(--shd-text-muted)}.stack-telemetry>div>b{color:var(--shd-danger);text-align:right}.stack-telemetry>div>b.maintained{color:var(--shd-success)}
|
|
@media(max-width:820px){.stack-telemetry>div{grid-template-columns:1fr 1fr}.stack-telemetry>div>strong{grid-column:1/-1}.stack-telemetry>div>b{text-align:left}}
|
|
@media(max-width:620px){header{display:grid}.weapon-calculation.incomplete>header{display:flex}.dps-grid{grid-template-columns:1fr}.dps-grid span{border-left:0;border-top:1px solid var(--shd-border)}.dps-grid span:first-child{border-top:0}footer{display:grid}.damage-grid span{min-height:44px}}
|
|
</style>
|