126 lines
5.8 KiB
Svelte
126 lines
5.8 KiB
Svelte
<script lang="ts">
|
||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||
import {
|
||
MOD_QUALITIES,
|
||
MOD_SECONDARY_CAPACITY,
|
||
MOD_SECONDARY_QUALITIES,
|
||
type ModSecondaryRange
|
||
} from '$lib/mods/secondary-pool';
|
||
|
||
let {
|
||
kind,
|
||
pool,
|
||
stats,
|
||
error,
|
||
saved = false,
|
||
resultToken = ''
|
||
}: {
|
||
kind: 'Weapon Mod' | 'Gear Mod';
|
||
pool: ModSecondaryRange[];
|
||
stats: Array<{ id: string; name: string; unit: string }>;
|
||
error?: string;
|
||
saved?: boolean;
|
||
resultToken?: string;
|
||
} = $props();
|
||
|
||
const poolId = $derived(kind === 'Weapon Mod' ? 'weapon-mod-secondary-pool' : 'gear-mod-secondary-pool');
|
||
const action = '?/saveSecondaryPool';
|
||
const qualityClass = (quality: string) => `quality-${quality.toLowerCase().replace('-', '')}`;
|
||
const blankSecondary = (): ModSecondaryRange => ({
|
||
statId: '',
|
||
values: Object.fromEntries(
|
||
MOD_SECONDARY_QUALITIES.map((quality) => [quality, {
|
||
level1Min: 0,
|
||
level1Max: 0,
|
||
level20Min: 0,
|
||
level20Max: 0
|
||
}])
|
||
) as ModSecondaryRange['values']
|
||
});
|
||
let editing = $state(false);
|
||
let draft = $state<ModSecondaryRange[]>([]);
|
||
let handledResultToken = $state('');
|
||
|
||
function openEditor() {
|
||
draft = pool.length ? structuredClone(pool) : [blankSecondary(), blankSecondary()];
|
||
editing = true;
|
||
}
|
||
|
||
function addSecondary() {
|
||
if (draft.length < 50) draft = [...draft, blankSecondary()];
|
||
}
|
||
|
||
function removeSecondary(index: number) {
|
||
if (draft.length > 2) draft = draft.filter((_, itemIndex) => itemIndex !== index);
|
||
}
|
||
|
||
$effect(() => {
|
||
if (saved && resultToken && resultToken !== handledResultToken) {
|
||
handledResultToken = resultToken;
|
||
editing = false;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<section class="mod-secondary-pool-registry">
|
||
<div class="selection-head">
|
||
<div>
|
||
<span>SHARED {kind.toUpperCase()} SECONDARY POOL</span>
|
||
<small>ONE CANONICAL POOL FOR EVERY REGULAR {kind.toUpperCase()}</small>
|
||
</div>
|
||
<button type="button" class="row-action" onclick={openEditor}>EDIT SHARED POOL →</button>
|
||
</div>
|
||
<div class="mod-secondary-capacity">
|
||
{#each MOD_QUALITIES as quality}
|
||
<span><b class={qualityClass(quality)}>{quality.toUpperCase()}</b><strong>{MOD_SECONDARY_CAPACITY[quality]}</strong><small>SECONDARY {MOD_SECONDARY_CAPACITY[quality] === 1 ? 'ROLL' : 'ROLLS'}</small></span>
|
||
{/each}
|
||
<span class="pool-total"><b>ATTRIBUTE POOL</b><strong>{pool.length}</strong><small>SHARED OPTIONS</small></span>
|
||
</div>
|
||
</section>
|
||
|
||
{#if editing}
|
||
<div class="dashboard-editor mod-secondary-pool-editor">
|
||
<div class="editor-title">
|
||
<div><p class="eyebrow">SHARED CONFIGURATION</p><h3>{kind.toUpperCase()} SECONDARY ATTRIBUTES</h3></div>
|
||
<button aria-label={`Close ${kind} secondary pool editor`} onclick={() => editing = false}>×</button>
|
||
</div>
|
||
<PendingForm {action}>
|
||
<input type="hidden" name="id" value={poolId}/>
|
||
<input type="hidden" name="name" value={`Shared ${kind} Secondary Pool`}/>
|
||
<input type="hidden" name="secondaryRanges" value={JSON.stringify(draft)}/>
|
||
<p class="console-note">Standard quality has no secondary roll. Specialized selects one option; Superior and High-end select two distinct options. Ranges scale linearly from Level 1 to Level 20.</p>
|
||
<div class="selection-head pool-editor-head">
|
||
<div><span>AVAILABLE ATTRIBUTES</span><small>SPECIALIZED · SUPERIOR · HIGH-END LEVEL ENDPOINTS</small></div>
|
||
<button type="button" class="row-action" onclick={addSecondary} disabled={draft.length >= 50}>ADD ATTRIBUTE +</button>
|
||
</div>
|
||
<div class="mod-secondary-pool-options">
|
||
{#each draft as range, index}
|
||
<div class="mod-secondary-pool-option">
|
||
<strong>{String(index + 1).padStart(2, '0')}</strong>
|
||
<SearchableAssignment
|
||
items={stats}
|
||
mode="single"
|
||
compact
|
||
subjectLabel="POSSIBLE ATTRIBUTE"
|
||
searchPlaceholder="Search attributes…"
|
||
excludedIds={draft.filter((_, itemIndex) => itemIndex !== index).map((item) => item.statId)}
|
||
bind:value={range.statId}
|
||
/>
|
||
{#each MOD_SECONDARY_QUALITIES as quality}
|
||
<div class="mod-secondary-quality-range">
|
||
<b class={qualityClass(quality)}>{quality.toUpperCase()}</b>
|
||
<label><span>LEVEL 1</span><div><input type="number" step="any" bind:value={range.values[quality].level1Min} aria-label={`${quality} ${stats.find((item) => item.id === range.statId)?.name ?? 'attribute'} Level 1 minimum`} required/><i>–</i><input type="number" step="any" bind:value={range.values[quality].level1Max} aria-label={`${quality} ${stats.find((item) => item.id === range.statId)?.name ?? 'attribute'} Level 1 maximum`} required/></div></label>
|
||
<label><span>LEVEL 20</span><div><input type="number" step="any" bind:value={range.values[quality].level20Min} aria-label={`${quality} ${stats.find((item) => item.id === range.statId)?.name ?? 'attribute'} Level 20 minimum`} required/><i>–</i><input type="number" step="any" bind:value={range.values[quality].level20Max} aria-label={`${quality} ${stats.find((item) => item.id === range.statId)?.name ?? 'attribute'} Level 20 maximum`} required/></div></label>
|
||
</div>
|
||
{/each}
|
||
<button type="button" aria-label={`Remove shared secondary attribute option ${index + 1}`} onclick={() => removeSecondary(index)} disabled={draft.length <= 2}>×</button>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{#if error}<p class="error">{error}</p>{/if}
|
||
<div class="form-actions"><button type="button" class="secondary" onclick={() => editing = false}>CANCEL</button><button class="primary">SAVE SHARED POOL →</button></div>
|
||
</PendingForm>
|
||
</div>
|
||
{/if}
|