102 lines
3.9 KiB
Svelte
102 lines
3.9 KiB
Svelte
<script lang="ts">
|
||
import SearchableAssignment from './SearchableAssignment.svelte';
|
||
import SegmentedChoice from './SegmentedChoice.svelte';
|
||
|
||
type RegistryItem = { id: string; name: string; pending?: boolean; proposerName?: string };
|
||
export type StructuredEffect = {
|
||
type: 'Attribute' | 'Ability' | 'Custom';
|
||
statId: string;
|
||
abilityId: string;
|
||
label: string;
|
||
value: number | string | null;
|
||
unit: string;
|
||
};
|
||
|
||
let {
|
||
effects = $bindable([]),
|
||
attributes,
|
||
abilities = [],
|
||
idPrefix = 'effect',
|
||
emptyLabel = 'NO STRUCTURED EFFECTS',
|
||
addLabel = 'ADD EFFECT',
|
||
allowTargetLabelFallback = false
|
||
}: {
|
||
effects?: StructuredEffect[];
|
||
attributes: RegistryItem[];
|
||
abilities?: RegistryItem[];
|
||
idPrefix?: string;
|
||
emptyLabel?: string;
|
||
addLabel?: string;
|
||
allowTargetLabelFallback?: boolean;
|
||
} = $props();
|
||
|
||
const effectTypes = $derived([
|
||
{ value: 'Attribute', label: 'ATTRIBUTE' },
|
||
...(abilities.length ? [{ value: 'Ability', label: 'ABILITY' }] : []),
|
||
{ value: 'Custom', label: 'CUSTOM' }
|
||
]);
|
||
const units = [
|
||
{ value: '', label: 'NONE' },
|
||
{ value: '%', label: '%' },
|
||
{ value: 's', label: 'SECONDS' },
|
||
{ value: 'Shots', label: 'SHOTS' },
|
||
{ value: 'm', label: 'METERS' }
|
||
];
|
||
const blank = (): StructuredEffect => ({
|
||
type: 'Custom',
|
||
statId: '',
|
||
abilityId: '',
|
||
label: '',
|
||
value: '',
|
||
unit: ''
|
||
});
|
||
const changeType = (index: number, type: string) => {
|
||
effects[index] = {
|
||
...effects[index],
|
||
type: type as StructuredEffect['type'],
|
||
statId: type === 'Attribute' ? effects[index].statId : '',
|
||
abilityId: type === 'Ability' ? effects[index].abilityId : ''
|
||
};
|
||
};
|
||
</script>
|
||
|
||
<div class="structured-effect-editor">
|
||
{#each effects as effect, index}
|
||
{@const labelCanUseTarget = allowTargetLabelFallback && ['Attribute', 'Ability'].includes(effect.type)}
|
||
<section class="structured-effect-row">
|
||
<header><strong>EFFECT {String(index + 1).padStart(2, '0')}</strong><button type="button" class="danger-link" onclick={() => effects = effects.filter((_, itemIndex) => itemIndex !== index)}>REMOVE</button></header>
|
||
<div class="structured-effect-grid">
|
||
<SegmentedChoice
|
||
legend="EFFECT TYPE"
|
||
name={`${idPrefix}-type-${index}`}
|
||
options={effectTypes}
|
||
bind:value={effect.type}
|
||
onchange={(values) => changeType(index, values[0])}
|
||
required
|
||
/>
|
||
{#if effect.type === 'Attribute'}
|
||
<div class="field"><span>ATTRIBUTE TARGET</span><SearchableAssignment items={attributes} mode="single" compact subjectLabel="ATTRIBUTE" searchPlaceholder="Search Attributes…" bind:value={effect.statId}/></div>
|
||
{:else if effect.type === 'Ability'}
|
||
<div class="field"><span>CLASS ABILITY TARGET</span><SearchableAssignment items={abilities} mode="single" compact subjectLabel="CLASS ABILITY" searchPlaceholder="Search abilities…" bind:value={effect.abilityId}/></div>
|
||
{:else}
|
||
<div class="effect-custom-target"><span>CUSTOM MECHANIC</span><small>Recorded for display now and calculation support later.</small></div>
|
||
{/if}
|
||
<label class="field effect-label">
|
||
EFFECT LABEL{labelCanUseTarget ? ' · OPTIONAL' : ''}
|
||
<input
|
||
bind:value={effect.label}
|
||
maxlength="100"
|
||
placeholder={labelCanUseTarget ? 'Uses the selected target name when blank' : 'e.g. Damage Reduction'}
|
||
required={!labelCanUseTarget}
|
||
/>
|
||
</label>
|
||
<label class="field">VALUE<input type="number" step="any" bind:value={effect.value} placeholder="OPTIONAL"/></label>
|
||
<SegmentedChoice legend="UNIT" name={`${idPrefix}-unit-${index}`} options={units} bind:value={effect.unit}/>
|
||
</div>
|
||
</section>
|
||
{:else}
|
||
<div class="structured-effect-empty"><strong>{emptyLabel}</strong><span>Add structured values when this mechanic should contribute to calculations or an ability preview.</span></div>
|
||
{/each}
|
||
<button type="button" class="secondary structured-effect-add" onclick={() => effects = [...effects, blank()]}>{addLabel} +</button>
|
||
</div>
|