129 lines
8.8 KiB
Svelte
129 lines
8.8 KiB
Svelte
<script lang="ts">
|
||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||
import {
|
||
SKILL_CHIP_QUALITIES,
|
||
SKILL_CHIP_SECONDARY_CAPACITY,
|
||
SKILL_CHIP_SECONDARY_QUALITIES,
|
||
type SkillChipProfile
|
||
} from '$lib/skill-chips/profiles';
|
||
|
||
let {
|
||
primaryProfiles,
|
||
primaryProfileBase,
|
||
secondaryPool,
|
||
stats,
|
||
primaryError,
|
||
secondaryError,
|
||
primarySaved = false,
|
||
secondarySaved = false,
|
||
resultToken = ''
|
||
}: {
|
||
primaryProfiles: SkillChipProfile[];
|
||
primaryProfileBase: SkillChipProfile[];
|
||
secondaryPool: SkillChipProfile[];
|
||
stats: Array<{ id: string; name: string; unit: string }>;
|
||
primaryError?: string;
|
||
secondaryError?: string;
|
||
primarySaved?: boolean;
|
||
secondarySaved?: boolean;
|
||
resultToken?: string;
|
||
} = $props();
|
||
|
||
const qualityClass = (quality: string) => `quality-${quality.toLowerCase().replace('-', '')}`;
|
||
const blankValues = (qualities: readonly string[]) => Object.fromEntries(
|
||
qualities.map((quality) => [quality, { min: '', max: '' }])
|
||
);
|
||
const hydrated = (profile: SkillChipProfile, qualities: readonly string[]) => ({
|
||
statId: profile.statId,
|
||
values: Object.fromEntries(qualities.map((quality) => [quality, {
|
||
min: profile.values[quality]?.min ?? '',
|
||
max: profile.values[quality]?.max ?? ''
|
||
}]))
|
||
});
|
||
const blankSecondary = () => ({ statId: '', values: blankValues(SKILL_CHIP_SECONDARY_QUALITIES) });
|
||
let primaryEditing = $state(false);
|
||
let secondaryEditing = $state(false);
|
||
let primaryDraft = $state<any[]>([]);
|
||
let secondaryDraft = $state<any[]>([]);
|
||
let handledPrimaryToken = $state('');
|
||
let handledSecondaryToken = $state('');
|
||
|
||
function openPrimary() {
|
||
primaryDraft = primaryProfiles.map((profile) => hydrated(profile, SKILL_CHIP_QUALITIES));
|
||
secondaryEditing = false;
|
||
primaryEditing = true;
|
||
}
|
||
function openSecondary() {
|
||
const loaded: any[] = secondaryPool.map((profile) =>
|
||
hydrated(profile, SKILL_CHIP_SECONDARY_QUALITIES)
|
||
);
|
||
while (loaded.length < 2) loaded.push(blankSecondary());
|
||
secondaryDraft = loaded;
|
||
primaryEditing = false;
|
||
secondaryEditing = true;
|
||
}
|
||
$effect(() => {
|
||
if (primarySaved && resultToken && resultToken !== handledPrimaryToken) {
|
||
handledPrimaryToken = resultToken;
|
||
primaryEditing = false;
|
||
}
|
||
if (secondarySaved && resultToken && resultToken !== handledSecondaryToken) {
|
||
handledSecondaryToken = resultToken;
|
||
secondaryEditing = false;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<div class="skill-chip-shared-config">
|
||
<section class="mod-secondary-pool-registry">
|
||
<div class="selection-head"><div><span>SHARED PRIMARY PROFILE</span><small>FOUR ABILITY-SCOPED STATS · ONE RANGE PER QUALITY</small></div><button type="button" class="row-action" onclick={openPrimary}>EDIT PRIMARY RANGES →</button></div>
|
||
<div class="mod-secondary-capacity">
|
||
{#each SKILL_CHIP_QUALITIES as quality}<span><b class={qualityClass(quality)}>{quality.toUpperCase()}</b><strong>{primaryProfiles.filter((profile) => profile.values[quality]).length}</strong><small>OF 4 CONFIGURED</small></span>{/each}
|
||
<span class="pool-total"><b>CALCULATION</b><strong>—</strong><small>NOT YET DEFINED</small></span>
|
||
</div>
|
||
</section>
|
||
<section class="mod-secondary-pool-registry">
|
||
<div class="selection-head"><div><span>SHARED SECONDARY POOL</span><small>ONE CANONICAL ATTRIBUTE POOL FOR EVERY SKILL CHIP</small></div><button type="button" class="row-action" onclick={openSecondary}>EDIT SHARED POOL →</button></div>
|
||
<div class="mod-secondary-capacity">
|
||
{#each SKILL_CHIP_QUALITIES as quality}<span><b class={qualityClass(quality)}>{quality.toUpperCase()}</b><strong>{SKILL_CHIP_SECONDARY_CAPACITY[quality]}</strong><small>SECONDARY {SKILL_CHIP_SECONDARY_CAPACITY[quality] === 1 ? 'ROLL' : 'ROLLS'}</small></span>{/each}
|
||
<span class="pool-total"><b>ATTRIBUTE POOL</b><strong>{secondaryPool.length}</strong><small>SHARED OPTIONS</small></span>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
{#if primaryEditing}
|
||
<div class="dashboard-editor skill-chip-profile-editor">
|
||
<div class="editor-title"><div><p class="eyebrow">SHARED CONFIGURATION</p><h3>SKILL CHIP PRIMARY RANGES</h3></div><button aria-label="Close primary profile editor" onclick={() => primaryEditing = false}>×</button></div>
|
||
<PendingForm action="?/savePrimaryProfile">
|
||
<input type="hidden" name="id" value="skill-chip-primary-profile"/><input type="hidden" name="name" value="Shared Skill Chip Primary Profile"/><input type="hidden" name="baseProfiles" value={JSON.stringify(primaryProfileBase)}/><input type="hidden" name="profiles" value={JSON.stringify(primaryDraft)}/>
|
||
<p class="console-note">Every Skill Chip rolls one of these four ability-scoped primary stats. Configure the direct minimum and maximum for each quality. Current combat calculations do not apply these values.</p>
|
||
<div class="skill-chip-profile-options fixed">
|
||
{#each primaryDraft as profile}
|
||
<div class="skill-chip-profile-option"><div><strong>{stats.find((item) => item.id === profile.statId)?.name ?? 'UNKNOWN ATTRIBUTE'}</strong><small>{stats.find((item) => item.id === profile.statId)?.unit || 'FLAT'} · ABILITY SCOPED</small></div>{#each SKILL_CHIP_QUALITIES as quality}<label><b class={qualityClass(quality)}>{quality.toUpperCase()}</b><span><input type="number" min="0" step="any" bind:value={profile.values[quality].min} aria-label={`${quality} ${stats.find((item) => item.id === profile.statId)?.name ?? 'attribute'} minimum`} required/><i>–</i><input type="number" min="0" step="any" bind:value={profile.values[quality].max} aria-label={`${quality} ${stats.find((item) => item.id === profile.statId)?.name ?? 'attribute'} maximum`} required/></span></label>{/each}</div>
|
||
{/each}
|
||
</div>
|
||
{#if primaryError}<p class="error">{primaryError}</p>{/if}
|
||
<div class="form-actions"><button type="button" class="secondary" onclick={() => primaryEditing = false}>CANCEL</button><button class="primary">SAVE PRIMARY PROFILE →</button></div>
|
||
</PendingForm>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if secondaryEditing}
|
||
<div class="dashboard-editor skill-chip-profile-editor">
|
||
<div class="editor-title"><div><p class="eyebrow">SHARED CONFIGURATION</p><h3>SKILL CHIP SECONDARY POOL</h3></div><button aria-label="Close secondary pool editor" onclick={() => secondaryEditing = false}>×</button></div>
|
||
<PendingForm action="?/saveSecondaryPool">
|
||
<input type="hidden" name="id" value="skill-chip-secondary-pool"/><input type="hidden" name="name" value="Shared Skill Chip Secondary Pool"/><input type="hidden" name="baseProfiles" value={JSON.stringify(secondaryPool)}/><input type="hidden" name="profiles" value={JSON.stringify(secondaryDraft)}/>
|
||
<p class="console-note">Standard has no secondary roll. Specialized selects one option; Superior and High-end select two distinct options. Skill Chips have no item level, so each quality uses one direct range.</p>
|
||
<div class="selection-head pool-editor-head"><div><span>AVAILABLE ATTRIBUTES</span><small>SPECIALIZED · SUPERIOR · HIGH-END RANGES</small></div><button type="button" class="row-action" onclick={() => secondaryDraft = [...secondaryDraft, blankSecondary()]} disabled={secondaryDraft.length >= 50}>ADD ATTRIBUTE +</button></div>
|
||
<div class="skill-chip-profile-options">
|
||
{#each secondaryDraft as profile, index}
|
||
<div class="skill-chip-profile-option selectable"><strong>{String(index + 1).padStart(2, '0')}</strong><SearchableAssignment items={stats} mode="single" compact subjectLabel="POSSIBLE ATTRIBUTE" searchPlaceholder="Search attributes…" excludedIds={secondaryDraft.filter((_, itemIndex) => itemIndex !== index).map((item) => item.statId)} bind:value={profile.statId}/>{#each SKILL_CHIP_SECONDARY_QUALITIES as quality}<label><b class={qualityClass(quality)}>{quality.toUpperCase()}</b><span><input type="number" min="0" step="any" bind:value={profile.values[quality].min} aria-label={`${quality} ${stats.find((item) => item.id === profile.statId)?.name ?? `option ${index + 1}`} minimum`} required/><i>–</i><input type="number" min="0" step="any" bind:value={profile.values[quality].max} aria-label={`${quality} ${stats.find((item) => item.id === profile.statId)?.name ?? `option ${index + 1}`} maximum`} required/></span></label>{/each}<button type="button" aria-label={`Remove secondary option ${index + 1}`} onclick={() => secondaryDraft = secondaryDraft.filter((_, itemIndex) => itemIndex !== index)} disabled={secondaryDraft.length <= 2}>×</button></div>
|
||
{/each}
|
||
</div>
|
||
{#if secondaryError}<p class="error">{secondaryError}</p>{/if}
|
||
<div class="form-actions"><button type="button" class="secondary" onclick={() => secondaryEditing = false}>CANCEL</button><button class="primary">SAVE SHARED POOL →</button></div>
|
||
</PendingForm>
|
||
</div>
|
||
{/if}
|