Files
shd.build/src/lib/components/SkillChipSharedProfiles.svelte
T
jb 57068b61cd
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s
feat: expand ISAC content modeling
2026-08-03 01:23:20 -04:00

129 lines
8.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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}