Files
shd.build/src/lib/components/builder/ConditionalEffectCard.svelte
T

139 lines
7.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
import RangeValueControl from '$lib/components/RangeValueControl.svelte';
import type { CalculationMode, CombatProfile, ConditionalEffectSelection, EffectMode } from '$lib/builder/loadout';
import type { ScenarioInputDefinition } from '$lib/calculations/mechanic';
export type ConditionalEffectState = ConditionalEffectSelection;
let {
id,
name,
description,
facts,
appliedState,
scenarioInputs = [],
effectState = $bindable<ConditionalEffectState>(),
calculationMode = 'custom',
disabled = false
}: {
id: string;
name: string;
description?: string;
facts: string[];
appliedState: string;
scenarioInputs?: ScenarioInputDefinition[];
effectState: ConditionalEffectState;
calculationMode?: CalculationMode;
disabled?: boolean;
} = $props();
let advanced = $state(false);
const modes = [
{ value: 'off', label: 'Off' },
{ value: 'peak', label: 'Peak' },
{ value: 'encounter', label: 'Encounter' }
];
function setMode(values: string[]) {
effectState = { ...effectState, mode: values[0] as EffectMode };
}
function updateOverride(key: keyof CombatProfile, value: number) {
effectState = { ...effectState, overrides: { ...effectState.overrides, [key]: value } };
}
function updateScenarioInput(key: string, value: number | boolean) {
effectState = {
...effectState,
inputs: { ...effectState.inputs, [key]: value }
};
}
function clearOverrides() {
effectState = { ...effectState, overrides: undefined, inputs: undefined };
}
</script>
<article class:disabled class="conditional-effect" aria-labelledby={`${id}-title`}>
<header>
<div>
<p>CONDITIONAL EFFECT</p>
<h3 id={`${id}-title`}>{name}</h3>
{#if description}<span>{description}</span>{/if}
</div>
<b class:active={effectState.mode !== 'off'}>{appliedState}</b>
</header>
<div class="facts" aria-label={`${name} facts`}>
{#each facts as fact}<span>{fact}</span>{/each}
</div>
{#if calculationMode === 'maximum'}
<div class="maximum-effect">
<strong>FULL EFFECT APPLIED</strong>
<span>Maximum stacks · best-case uptime</span>
</div>
{:else}
<fieldset class="mode-control" disabled={disabled}>
<SegmentedChoice legend={`${name} calculation mode`} name={`${id}-mode`} options={modes} value={effectState.mode} columns={3} onchange={setMode} />
</fieldset>
{/if}
{#if calculationMode === 'custom' && effectState.mode === 'encounter'}
<div class="advanced-head">
<button type="button" aria-expanded={advanced} aria-controls={`${id}-overrides`} onclick={() => advanced = !advanced} disabled={disabled}>
{advanced ? 'HIDE' : 'ADVANCED'} OVERRIDES <span aria-hidden="true">{advanced ? '−' : '+'}</span>
</button>
{#if effectState.overrides || effectState.inputs}<button class="clear" type="button" onclick={clearOverrides} disabled={disabled}>RESET</button>{/if}
</div>
{#if advanced}
<div class="override-grid" id={`${id}-overrides`}>
<RangeValueControl label="Hit rate" min={0} max={100} value={effectState.overrides?.hitRate ?? 100} onchange={(value) => updateOverride('hitRate', value)} />
<RangeValueControl label="Shots per burst" min={1} max={200} step={1} value={effectState.overrides?.shotsPerBurst ?? 30} onchange={(value) => updateOverride('shotsPerBurst', value)} />
<RangeValueControl label="Pause between bursts" min={0} max={15} step={0.1} value={effectState.overrides?.burstPauseSeconds ?? 0} onchange={(value) => updateOverride('burstPauseSeconds', value)} />
{#each scenarioInputs as input}
<div class="program-override">
{#if input.kind === 'toggle'}
<SegmentedChoice
legend={input.label}
name={`${id}-scenario-${input.key}`}
options={[{ value: 'false', label: 'OFF' }, { value: 'true', label: 'ON' }]}
value={String(effectState.inputs?.[input.key] ?? input.defaultValue ?? false)}
onchange={(values) => updateScenarioInput(input.key, values[0] === 'true')}
/>
{:else}
<RangeValueControl
label={input.label}
min={input.min ?? 0}
max={input.max ?? 100}
step={input.kind === 'slider' ? 1 : 0.1}
value={Number(effectState.inputs?.[input.key] ?? input.defaultValue ?? input.min ?? 0)}
onchange={(value) => updateScenarioInput(input.key, value)}
/>
{/if}
{#if input.description}<small>{input.description}</small>{/if}
</div>
{/each}
</div>
{/if}
{/if}
</article>
<style>
.conditional-effect{display:grid;gap:var(--shd-gap-sm);padding:var(--shd-gap-md);border:1px solid var(--shd-border);background:var(--shd-surface)}
.conditional-effect.disabled{opacity:.55;pointer-events:none}
header{display:flex;justify-content:space-between;align-items:start;gap:var(--shd-gap-md)}
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;color:var(--shd-text);font-size:var(--shd-type-body);text-transform:uppercase;letter-spacing:.7px}
header span{display:block;margin-top:4px;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
header>b{flex:0 0 auto;padding:5px 7px;border:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.8px;white-space:nowrap}
header>b.active{border-color:#74552a;color:var(--shd-orange);background:#1d180e}
.facts{display:flex;flex-wrap:wrap;gap:5px}.facts span{padding:4px 6px;border:1px solid var(--shd-border-soft);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
.mode-control{margin:0;padding:0;border:0}.advanced-head{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);border-top:1px solid var(--shd-border-soft);padding-top:var(--shd-gap-sm)}
.maximum-effect{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);padding:7px 8px;border:1px solid #74552a;background:#1d180e}
.maximum-effect strong,.maximum-effect span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.maximum-effect strong{color:var(--shd-orange)}.maximum-effect span{color:var(--shd-text-muted)}
button{min-height:40px;border:1px solid var(--shd-border);background:transparent;color:var(--shd-text-muted);padding:0 10px;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px;cursor:pointer;transition:var(--shd-transition)}
button:hover,button:focus-visible{border-color:var(--shd-orange);color:var(--shd-orange)}button:active{transform:translateY(1px);filter:brightness(.88)}button:focus-visible{outline:2px solid var(--shd-focus-ring);outline-offset:2px}.clear{border-color:#563734;color:var(--shd-danger)}.clear:hover,.clear:focus-visible{border-color:var(--shd-danger);color:var(--shd-danger)}
.override-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--shd-gap-md);padding:var(--shd-gap-sm);border:1px solid var(--shd-border-soft);background:var(--shd-surface-raised)}
.program-override{display:grid;gap:5px}.program-override small{color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
@media(max-width:620px){header{display:grid}.override-grid{grid-template-columns:1fr}.maximum-effect{align-items:start;flex-direction:column}button{min-height:44px}}
@media(prefers-reduced-motion:reduce){button{transition:none}}
</style>