139 lines
7.3 KiB
Svelte
139 lines
7.3 KiB
Svelte
<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>
|