feat: add source-owned combat mechanic programs
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
<script lang="ts">
|
||||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||||
import RangeValueControl from '$lib/components/RangeValueControl.svelte';
|
||||
import type { CombatProfile } from './CombatAssumptions.svelte';
|
||||
|
||||
export type EffectMode = 'off' | 'peak' | 'encounter';
|
||||
export type ConditionalEffectState = {
|
||||
mode: EffectMode;
|
||||
overrides?: Partial<CombatProfile>;
|
||||
};
|
||||
import type { CalculationMode, CombatProfile, ConditionalEffectSelection, EffectMode } from '$lib/builder/loadout';
|
||||
import type { ScenarioInputDefinition } from '$lib/calculations/mechanic';
|
||||
export type ConditionalEffectState = ConditionalEffectSelection;
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -15,7 +11,9 @@
|
||||
description,
|
||||
facts,
|
||||
appliedState,
|
||||
scenarioInputs = [],
|
||||
effectState = $bindable<ConditionalEffectState>(),
|
||||
calculationMode = 'custom',
|
||||
disabled = false
|
||||
}: {
|
||||
id: string;
|
||||
@@ -23,7 +21,9 @@
|
||||
description?: string;
|
||||
facts: string[];
|
||||
appliedState: string;
|
||||
scenarioInputs?: ScenarioInputDefinition[];
|
||||
effectState: ConditionalEffectState;
|
||||
calculationMode?: CalculationMode;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
@@ -42,8 +42,15 @@
|
||||
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 };
|
||||
effectState = { ...effectState, overrides: undefined, inputs: undefined };
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -59,20 +66,51 @@
|
||||
<div class="facts" aria-label={`${name} facts`}>
|
||||
{#each facts as fact}<span>{fact}</span>{/each}
|
||||
</div>
|
||||
<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 effectState.mode === 'encounter'}
|
||||
{#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}<button class="clear" type="button" onclick={clearOverrides} disabled={disabled}>RESET</button>{/if}
|
||||
{#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="Firing activity" min={0} max={100} value={effectState.overrides?.firingActivity ?? 100} onchange={(value) => updateOverride('firingActivity', 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}
|
||||
@@ -89,9 +127,12 @@
|
||||
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)}
|
||||
@media(max-width:620px){header{display:grid}.override-grid{grid-template-columns:1fr}button{min-height:44px}}
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user