auto-redistribute skill levels when artifact level changes
This commit is contained in:
parent
f34f2c4dc9
commit
f55303039c
3 changed files with 272 additions and 119 deletions
|
|
@ -4,20 +4,19 @@
|
||||||
import type {
|
import type {
|
||||||
ArtifactInstance,
|
ArtifactInstance,
|
||||||
ArtifactSkillInstance,
|
ArtifactSkillInstance,
|
||||||
ArtifactSkill,
|
ArtifactSkill
|
||||||
ArtifactGrade
|
|
||||||
} from '$lib/types/api/artifact'
|
} from '$lib/types/api/artifact'
|
||||||
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
|
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
|
||||||
import { createQuery } from '@tanstack/svelte-query'
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
||||||
import { usePaneStack, type PaneConfig, type ElementType } from '$lib/stores/paneStack.svelte'
|
import { usePaneStack, type PaneConfig } from '$lib/stores/paneStack.svelte'
|
||||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||||
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
||||||
import Select from '$lib/components/ui/Select.svelte'
|
import Select from '$lib/components/ui/Select.svelte'
|
||||||
import Slider from '$lib/components/ui/Slider.svelte'
|
import Input from '$lib/components/ui/Input.svelte'
|
||||||
import ArtifactSkillRow from './ArtifactSkillRow.svelte'
|
import ArtifactSkillRow from './ArtifactSkillRow.svelte'
|
||||||
import ArtifactModifierList from './ArtifactModifierList.svelte'
|
import ArtifactModifierList from './ArtifactModifierList.svelte'
|
||||||
import ArtifactGradeDisplay from './ArtifactGradeDisplay.svelte'
|
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** The artifact instance being edited */
|
/** The artifact instance being edited */
|
||||||
|
|
@ -34,10 +33,63 @@
|
||||||
const paneStack = usePaneStack()
|
const paneStack = usePaneStack()
|
||||||
|
|
||||||
// Local state for edits
|
// Local state for edits
|
||||||
|
// Use type assertion since this pane is used with CollectionArtifact which has nickname
|
||||||
|
let nickname = $state((artifact as { nickname?: string }).nickname ?? '')
|
||||||
let element = $state(artifact.element)
|
let element = $state(artifact.element)
|
||||||
let level = $state(artifact.level)
|
let level = $state(artifact.level)
|
||||||
let proficiency = $state(artifact.proficiency)
|
let proficiency = $state(artifact.proficiency)
|
||||||
let skills = $state<(ArtifactSkillInstance | null)[]>([...artifact.skills])
|
let skills = $state<(ArtifactSkillInstance | null)[]>(initializeSkillLevels([...artifact.skills]))
|
||||||
|
|
||||||
|
// Initialize skill levels to meet the constraint (artifact.level + 3)
|
||||||
|
// This handles cases where imported data has incorrect skill levels
|
||||||
|
function initializeSkillLevels(
|
||||||
|
inputSkills: (ArtifactSkillInstance | null)[]
|
||||||
|
): (ArtifactSkillInstance | null)[] {
|
||||||
|
// Skip for quirk artifacts
|
||||||
|
if (artifact.artifact?.rarity === 'quirk') return inputSkills
|
||||||
|
|
||||||
|
const targetSum = artifact.level + 3
|
||||||
|
const currentSum = inputSkills.reduce((sum, s) => sum + (s?.level ?? 0), 0)
|
||||||
|
const diff = targetSum - currentSum
|
||||||
|
|
||||||
|
if (diff === 0) return inputSkills
|
||||||
|
|
||||||
|
// Need to adjust skill levels
|
||||||
|
const adjusted = [...inputSkills]
|
||||||
|
|
||||||
|
if (diff > 0) {
|
||||||
|
// Need to add points
|
||||||
|
let remaining = diff
|
||||||
|
for (let i = 0; i < 4 && remaining > 0; i++) {
|
||||||
|
const skill = adjusted[i]
|
||||||
|
if (skill) {
|
||||||
|
const canAdd = Math.min(5 - skill.level, remaining)
|
||||||
|
if (canAdd > 0) {
|
||||||
|
adjusted[i] = { ...skill, level: skill.level + canAdd }
|
||||||
|
remaining -= canAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Need to remove points
|
||||||
|
let remaining = Math.abs(diff)
|
||||||
|
const indices = [0, 1, 2, 3]
|
||||||
|
.filter((i) => adjusted[i] !== null)
|
||||||
|
.sort((a, b) => (adjusted[b]?.level ?? 0) - (adjusted[a]?.level ?? 0))
|
||||||
|
|
||||||
|
for (const i of indices) {
|
||||||
|
if (remaining <= 0) break
|
||||||
|
const skill = adjusted[i]
|
||||||
|
if (skill && skill.level > 1) {
|
||||||
|
const canRemove = Math.min(skill.level - 1, remaining)
|
||||||
|
adjusted[i] = { ...skill, level: skill.level - canRemove }
|
||||||
|
remaining -= canRemove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjusted
|
||||||
|
}
|
||||||
|
|
||||||
// Derived values
|
// Derived values
|
||||||
const artifactData = $derived(artifact.artifact)
|
const artifactData = $derived(artifact.artifact)
|
||||||
|
|
@ -66,6 +118,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
// Convert numeric element to ElementType string
|
// Convert numeric element to ElementType string
|
||||||
|
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||||
const elementTypeMap: Record<number, ElementType> = {
|
const elementTypeMap: Record<number, ElementType> = {
|
||||||
1: 'wind',
|
1: 'wind',
|
||||||
2: 'fire',
|
2: 'fire',
|
||||||
|
|
@ -74,7 +127,7 @@
|
||||||
5: 'dark',
|
5: 'dark',
|
||||||
6: 'light'
|
6: 'light'
|
||||||
}
|
}
|
||||||
const elementType = $derived(elementTypeMap[element] ?? undefined)
|
const elementType = $derived(elementTypeMap[element])
|
||||||
|
|
||||||
// Level options (1-5 for standard, fixed at 1 for quirk)
|
// Level options (1-5 for standard, fixed at 1 for quirk)
|
||||||
const levelOptions = $derived(
|
const levelOptions = $derived(
|
||||||
|
|
@ -103,13 +156,6 @@
|
||||||
{ value: 10, label: 'Katana' }
|
{ value: 10, label: 'Katana' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Get proficiency display name
|
|
||||||
function getProficiencyName(profValue: number | null | undefined): string {
|
|
||||||
if (!profValue) return '—'
|
|
||||||
const option = proficiencyOptions.find((o) => o.value === profValue)
|
|
||||||
return option?.label ?? '—'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push modifier selection pane for a specific slot
|
// Push modifier selection pane for a specific slot
|
||||||
function handleSelectModifier(slot: number) {
|
function handleSelectModifier(slot: number) {
|
||||||
const config: PaneConfig = {
|
const config: PaneConfig = {
|
||||||
|
|
@ -119,6 +165,7 @@
|
||||||
props: {
|
props: {
|
||||||
slot,
|
slot,
|
||||||
selectedModifier: skills[slot - 1]?.modifier,
|
selectedModifier: skills[slot - 1]?.modifier,
|
||||||
|
element: elementType,
|
||||||
onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill)
|
onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,10 +186,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
skills = newSkills
|
skills = newSkills
|
||||||
notifyUpdate()
|
|
||||||
|
|
||||||
// Pop back to the edit pane
|
// Pop back to the edit pane BEFORE notifying, so the header update targets the right pane
|
||||||
paneStack.pop()
|
paneStack.pop()
|
||||||
|
notifyUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle skill updates from skill row
|
// Handle skill updates from skill row
|
||||||
|
|
@ -164,13 +211,64 @@
|
||||||
notifyUpdate()
|
notifyUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle level change
|
// Handle level change - redistributes skill levels to maintain constraint
|
||||||
function handleLevelChange(newLevel: number | undefined) {
|
function handleLevelChange(newLevel: number | undefined) {
|
||||||
if (newLevel === undefined) return
|
if (newLevel === undefined) return
|
||||||
|
|
||||||
|
const oldBudget = level + 3
|
||||||
|
const newBudget = newLevel + 3
|
||||||
|
const budgetDiff = newBudget - oldBudget
|
||||||
|
|
||||||
level = newLevel
|
level = newLevel
|
||||||
|
|
||||||
|
// If budget increased, distribute extra points to skills
|
||||||
|
// If budget decreased, remove points from skills (starting from highest)
|
||||||
|
if (budgetDiff !== 0 && !isQuirk) {
|
||||||
|
redistributeSkillLevels(budgetDiff)
|
||||||
|
}
|
||||||
|
|
||||||
notifyUpdate()
|
notifyUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redistribute skill levels when artifact level changes
|
||||||
|
function redistributeSkillLevels(budgetDiff: number) {
|
||||||
|
const newSkills = [...skills]
|
||||||
|
let remaining = budgetDiff
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
// Add points: distribute to skills that can accept more (max 5)
|
||||||
|
for (let i = 0; i < 4 && remaining > 0; i++) {
|
||||||
|
const skill = newSkills[i]
|
||||||
|
if (skill) {
|
||||||
|
const canAdd = Math.min(5 - skill.level, remaining)
|
||||||
|
if (canAdd > 0) {
|
||||||
|
newSkills[i] = { ...skill, level: skill.level + canAdd }
|
||||||
|
remaining -= canAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove points: take from skills with highest levels first
|
||||||
|
remaining = Math.abs(remaining)
|
||||||
|
// Sort indices by skill level descending
|
||||||
|
const indices = [0, 1, 2, 3]
|
||||||
|
.filter((i) => newSkills[i] !== null)
|
||||||
|
.sort((a, b) => (newSkills[b]?.level ?? 0) - (newSkills[a]?.level ?? 0))
|
||||||
|
|
||||||
|
for (const i of indices) {
|
||||||
|
if (remaining <= 0) break
|
||||||
|
const skill = newSkills[i]
|
||||||
|
if (skill && skill.level > 1) {
|
||||||
|
const canRemove = Math.min(skill.level - 1, remaining)
|
||||||
|
newSkills[i] = { ...skill, level: skill.level - canRemove }
|
||||||
|
remaining -= canRemove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skills = newSkills
|
||||||
|
}
|
||||||
|
|
||||||
// Handle proficiency change
|
// Handle proficiency change
|
||||||
function handleProficiencyChange(newProficiency: number | undefined) {
|
function handleProficiencyChange(newProficiency: number | undefined) {
|
||||||
if (newProficiency === undefined) return
|
if (newProficiency === undefined) return
|
||||||
|
|
@ -178,11 +276,19 @@
|
||||||
notifyUpdate()
|
notifyUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle nickname change
|
||||||
|
function handleNicknameChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
nickname = target.value
|
||||||
|
notifyUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
// Notify parent of updates
|
// Notify parent of updates
|
||||||
function notifyUpdate() {
|
function notifyUpdate() {
|
||||||
if (!onUpdate) return
|
if (!onUpdate) return
|
||||||
|
|
||||||
const updates: Partial<ArtifactInstance> = {
|
const updates: Partial<ArtifactInstance> = {
|
||||||
|
nickname: nickname.trim() || undefined,
|
||||||
element,
|
element,
|
||||||
level,
|
level,
|
||||||
skills: [...skills]
|
skills: [...skills]
|
||||||
|
|
@ -194,17 +300,28 @@
|
||||||
|
|
||||||
onUpdate(updates)
|
onUpdate(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current grade (from artifact or could be recalculated)
|
|
||||||
const currentGrade: ArtifactGrade = $derived(artifact.grade)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="artifact-edit-pane">
|
<div class="artifact-edit-pane">
|
||||||
<DetailsSection title="Base Properties">
|
<DetailsSection title="Basic Info">
|
||||||
|
<DetailRow label="Nickname" noHover>
|
||||||
|
{#if disabled}
|
||||||
|
<span>{nickname || '—'}</span>
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
class="nickname-input"
|
||||||
|
value={nickname}
|
||||||
|
oninput={handleNicknameChange}
|
||||||
|
placeholder="Optional nickname"
|
||||||
|
maxLength={50}
|
||||||
|
contained
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</DetailRow>
|
||||||
{#if canChangeProficiency}
|
{#if canChangeProficiency}
|
||||||
<DetailRow label="Proficiency" noHover>
|
<DetailRow label="Proficiency" noHover>
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<span>{getProficiencyName(proficiency)}</span>
|
<ProficiencyLabel {proficiency} size="medium" />
|
||||||
{:else}
|
{:else}
|
||||||
<Select
|
<Select
|
||||||
options={proficiencyOptions}
|
options={proficiencyOptions}
|
||||||
|
|
@ -218,7 +335,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
{:else}
|
{:else}
|
||||||
<DetailRow label="Proficiency" value={getProficiencyName(artifactData.proficiency)} />
|
<DetailRow label="Proficiency" noHover>
|
||||||
|
<ProficiencyLabel proficiency={artifactData.proficiency} size="medium" />
|
||||||
|
</DetailRow>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailRow label="Element" noHover>
|
<DetailRow label="Element" noHover>
|
||||||
|
|
@ -244,18 +363,13 @@
|
||||||
{#if disabled || isQuirk}
|
{#if disabled || isQuirk}
|
||||||
<span>{level}</span>
|
<span>{level}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="level-slider">
|
<Select
|
||||||
<Slider
|
options={levelOptions}
|
||||||
value={level}
|
value={level}
|
||||||
onValueChange={handleLevelChange}
|
onValueChange={handleLevelChange}
|
||||||
min={1}
|
contained
|
||||||
max={5}
|
{disabled}
|
||||||
step={1}
|
/>
|
||||||
element={elementType}
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
<span class="level-value">{level}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
</DetailsSection>
|
</DetailsSection>
|
||||||
|
|
@ -267,6 +381,8 @@
|
||||||
<ArtifactSkillRow
|
<ArtifactSkillRow
|
||||||
{slot}
|
{slot}
|
||||||
skill={skills[slot - 1] ?? null}
|
skill={skills[slot - 1] ?? null}
|
||||||
|
allSkills={skills}
|
||||||
|
artifactLevel={level}
|
||||||
availableSkills={getSkillsForSlot(slot)}
|
availableSkills={getSkillsForSlot(slot)}
|
||||||
onSelectModifier={() => handleSelectModifier(slot)}
|
onSelectModifier={() => handleSelectModifier(slot)}
|
||||||
onUpdateSkill={(update) => handleUpdateSkill(slot, update)}
|
onUpdateSkill={(update) => handleUpdateSkill(slot, update)}
|
||||||
|
|
@ -276,12 +392,6 @@
|
||||||
</div>
|
</div>
|
||||||
</DetailsSection>
|
</DetailsSection>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailsSection title="Grade">
|
|
||||||
<div class="grade-section">
|
|
||||||
<ArtifactGradeDisplay grade={currentGrade} />
|
|
||||||
</div>
|
|
||||||
</DetailsSection>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -298,7 +408,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.artifact-edit-pane .select.medium) {
|
:global(.artifact-edit-pane .select.medium) {
|
||||||
min-width: 120px;
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nickname-input) {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nickname-input input) {
|
||||||
|
padding: spacing.$unit spacing.$unit-4x spacing.$unit calc(spacing.$unit * 1.5) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-display {
|
.element-display {
|
||||||
|
|
@ -314,28 +432,9 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-slider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: spacing.$unit;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.level-value {
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
min-width: spacing.$unit-2x;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skills-list {
|
.skills-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: spacing.$unit;
|
gap: spacing.$unit;
|
||||||
padding: 0 spacing.$unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-section {
|
|
||||||
padding: spacing.$unit;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
artifact: CollectionArtifact
|
artifact: CollectionArtifact
|
||||||
|
/** Called when artifact is saved, with the updated artifact data */
|
||||||
|
onSaved?: (updatedArtifact: CollectionArtifact) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { artifact }: Props = $props()
|
let { artifact, onSaved }: Props = $props()
|
||||||
|
|
||||||
const paneStack = usePaneStack()
|
const paneStack = usePaneStack()
|
||||||
|
|
||||||
|
|
@ -49,20 +51,40 @@
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
if (!pendingUpdates) return
|
if (!pendingUpdates) return
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
nickname: pendingUpdates.nickname,
|
||||||
|
element: pendingUpdates.element,
|
||||||
|
level: pendingUpdates.level,
|
||||||
|
proficiency: pendingUpdates.proficiency,
|
||||||
|
skill1: pendingUpdates.skills?.[0] ?? undefined,
|
||||||
|
skill2: pendingUpdates.skills?.[1] ?? undefined,
|
||||||
|
skill3: pendingUpdates.skills?.[2] ?? undefined,
|
||||||
|
skill4: pendingUpdates.skills?.[3] ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: Log what we're sending
|
||||||
|
const skillLevelSum = [input.skill1, input.skill2, input.skill3, input.skill4]
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((sum, s) => sum + (s?.level ?? 0), 0)
|
||||||
|
const expectedSum = (input.level ?? artifact.level) + 3
|
||||||
|
console.log('[CollectionArtifactEditPane] Saving artifact:', {
|
||||||
|
id: artifact.id,
|
||||||
|
input,
|
||||||
|
skillLevelSum,
|
||||||
|
expectedSum,
|
||||||
|
constraintMet: skillLevelSum === expectedSum
|
||||||
|
})
|
||||||
|
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
id: artifact.id,
|
id: artifact.id,
|
||||||
input: {
|
input
|
||||||
element: pendingUpdates.element,
|
|
||||||
level: pendingUpdates.level,
|
|
||||||
proficiency: pendingUpdates.proficiency,
|
|
||||||
skill1: pendingUpdates.skills?.[0] ?? undefined,
|
|
||||||
skill2: pendingUpdates.skills?.[1] ?? undefined,
|
|
||||||
skill3: pendingUpdates.skills?.[2] ?? undefined,
|
|
||||||
skill4: pendingUpdates.skills?.[3] ?? undefined
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
onSuccess: () => {
|
onSuccess: (updatedArtifact) => {
|
||||||
|
onSaved?.(updatedArtifact)
|
||||||
paneStack.pop()
|
paneStack.pop()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('[CollectionArtifactEditPane] Save failed:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -78,9 +100,9 @@
|
||||||
return () => sidebar.clearAction()
|
return () => sidebar.clearAction()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reactively update header when state changes
|
// Reactively update header when state changes or when returning from sub-pane
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const _ = [hasChanges, updateMutation.isPending]
|
const _ = [hasChanges, updateMutation.isPending, paneStack.panes.length]
|
||||||
untrack(() => updateHeader())
|
untrack(() => updateHeader())
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@
|
||||||
* 5. Configure skills (for standard artifacts only, uses pane stack)
|
* 5. Configure skills (for standard artifacts only, uses pane stack)
|
||||||
*/
|
*/
|
||||||
import { onMount, untrack } from 'svelte'
|
import { onMount, untrack } from 'svelte'
|
||||||
import type { Artifact, ArtifactSkill, ArtifactSkillInstance, CollectionArtifactInput } from '$lib/types/api/artifact'
|
import type {
|
||||||
|
Artifact,
|
||||||
|
ArtifactSkill,
|
||||||
|
ArtifactSkillInstance,
|
||||||
|
CollectionArtifactInput
|
||||||
|
} from '$lib/types/api/artifact'
|
||||||
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
|
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
|
||||||
import { createQuery } from '@tanstack/svelte-query'
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
||||||
|
|
@ -22,7 +27,6 @@
|
||||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||||
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
||||||
import Select from '$lib/components/ui/Select.svelte'
|
import Select from '$lib/components/ui/Select.svelte'
|
||||||
import Slider from '$lib/components/ui/Slider.svelte'
|
|
||||||
import Input from '$lib/components/ui/Input.svelte'
|
import Input from '$lib/components/ui/Input.svelte'
|
||||||
import ArtifactSkillRow from '$lib/components/artifact/ArtifactSkillRow.svelte'
|
import ArtifactSkillRow from '$lib/components/artifact/ArtifactSkillRow.svelte'
|
||||||
import ArtifactModifierList from '$lib/components/artifact/ArtifactModifierList.svelte'
|
import ArtifactModifierList from '$lib/components/artifact/ArtifactModifierList.svelte'
|
||||||
|
|
@ -63,7 +67,7 @@
|
||||||
3: '#5cb7ec', // Water - blue
|
3: '#5cb7ec', // Water - blue
|
||||||
4: '#ec985c', // Earth - orange/brown
|
4: '#ec985c', // Earth - orange/brown
|
||||||
5: '#c65cec', // Dark - purple
|
5: '#c65cec', // Dark - purple
|
||||||
6: '#c59c0c' // Light - gold/yellow
|
6: '#c59c0c' // Light - gold/yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proficiency options - matches database enum values
|
// Proficiency options - matches database enum values
|
||||||
|
|
@ -94,7 +98,7 @@
|
||||||
// Standard artifacts have a fixed proficiency, quirk artifacts match any proficiency
|
// Standard artifacts have a fixed proficiency, quirk artifacts match any proficiency
|
||||||
const filteredArtifacts = $derived.by(() => {
|
const filteredArtifacts = $derived.by(() => {
|
||||||
if (!artifactsQuery.data || proficiency === undefined) return []
|
if (!artifactsQuery.data || proficiency === undefined) return []
|
||||||
return artifactsQuery.data.filter(a => {
|
return artifactsQuery.data.filter((a) => {
|
||||||
// Quirk artifacts have null proficiency - they work with any proficiency
|
// Quirk artifacts have null proficiency - they work with any proficiency
|
||||||
if (a.proficiency === null) return true
|
if (a.proficiency === null) return true
|
||||||
// Standard artifacts match their fixed proficiency
|
// Standard artifacts match their fixed proficiency
|
||||||
|
|
@ -104,16 +108,14 @@
|
||||||
|
|
||||||
// Build artifact options for dropdown (filtered by proficiency)
|
// Build artifact options for dropdown (filtered by proficiency)
|
||||||
const artifactOptions = $derived.by(() => {
|
const artifactOptions = $derived.by(() => {
|
||||||
return filteredArtifacts.map(a => ({
|
return filteredArtifacts.map((a) => ({
|
||||||
value: a.id,
|
value: a.id,
|
||||||
label: typeof a.name === 'string' ? a.name : (a.name.en || a.name.ja || '—')
|
label: typeof a.name === 'string' ? a.name : a.name.en || a.name.ja || '—'
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Selected artifact data
|
// Selected artifact data
|
||||||
const selectedArtifact = $derived(
|
const selectedArtifact = $derived(artifactsQuery.data?.find((a) => a.id === selectedArtifactId))
|
||||||
artifactsQuery.data?.find(a => a.id === selectedArtifactId)
|
|
||||||
)
|
|
||||||
const isQuirk = $derived(selectedArtifact ? isQuirkArtifact(selectedArtifact) : false)
|
const isQuirk = $derived(selectedArtifact ? isQuirkArtifact(selectedArtifact) : false)
|
||||||
|
|
||||||
// Level options (1-5 for standard, fixed at 1 for quirk)
|
// Level options (1-5 for standard, fixed at 1 for quirk)
|
||||||
|
|
@ -145,6 +147,7 @@
|
||||||
props: {
|
props: {
|
||||||
slot,
|
slot,
|
||||||
selectedModifier: skills[slot - 1]?.modifier,
|
selectedModifier: skills[slot - 1]?.modifier,
|
||||||
|
element: elementType,
|
||||||
onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill)
|
onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,17 +198,69 @@
|
||||||
// Reset skills when artifact changes
|
// Reset skills when artifact changes
|
||||||
skills = [null, null, null, null]
|
skills = [null, null, null, null]
|
||||||
// Reset level for quirk
|
// Reset level for quirk
|
||||||
const artifact = artifactsQuery.data?.find(a => a.id === newArtifactId)
|
const artifact = artifactsQuery.data?.find((a) => a.id === newArtifactId)
|
||||||
if (artifact && isQuirkArtifact(artifact)) {
|
if (artifact && isQuirkArtifact(artifact)) {
|
||||||
level = 1
|
level = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle level change - redistributes skill levels to maintain constraint
|
||||||
|
function handleLevelChange(newLevel: number | undefined) {
|
||||||
|
if (newLevel === undefined) return
|
||||||
|
|
||||||
|
const oldBudget = level + 3
|
||||||
|
const newBudget = newLevel + 3
|
||||||
|
const budgetDiff = newBudget - oldBudget
|
||||||
|
|
||||||
|
level = newLevel
|
||||||
|
|
||||||
|
// If budget changed and we have skills, redistribute levels
|
||||||
|
if (budgetDiff !== 0 && !isQuirk) {
|
||||||
|
redistributeSkillLevels(budgetDiff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redistribute skill levels when artifact level changes
|
||||||
|
function redistributeSkillLevels(budgetDiff: number) {
|
||||||
|
const newSkills = [...skills]
|
||||||
|
let remaining = budgetDiff
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
// Add points: distribute to skills that can accept more (max 5)
|
||||||
|
for (let i = 0; i < 4 && remaining > 0; i++) {
|
||||||
|
const skill = newSkills[i]
|
||||||
|
if (skill) {
|
||||||
|
const canAdd = Math.min(5 - skill.level, remaining)
|
||||||
|
if (canAdd > 0) {
|
||||||
|
newSkills[i] = { ...skill, level: skill.level + canAdd }
|
||||||
|
remaining -= canAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove points: take from skills with highest levels first
|
||||||
|
remaining = Math.abs(remaining)
|
||||||
|
const indices = [0, 1, 2, 3]
|
||||||
|
.filter((i) => newSkills[i] !== null)
|
||||||
|
.sort((a, b) => (newSkills[b]?.level ?? 0) - (newSkills[a]?.level ?? 0))
|
||||||
|
|
||||||
|
for (const i of indices) {
|
||||||
|
if (remaining <= 0) break
|
||||||
|
const skill = newSkills[i]
|
||||||
|
if (skill && skill.level > 1) {
|
||||||
|
const canRemove = Math.min(skill.level - 1, remaining)
|
||||||
|
newSkills[i] = { ...skill, level: skill.level - canRemove }
|
||||||
|
remaining -= canRemove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skills = newSkills
|
||||||
|
}
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
const isValid = $derived(
|
const isValid = $derived(
|
||||||
proficiency !== undefined &&
|
proficiency !== undefined && element !== undefined && selectedArtifactId !== undefined
|
||||||
element !== undefined &&
|
|
||||||
selectedArtifactId !== undefined
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Convert numeric element to ElementType string for button styling
|
// Convert numeric element to ElementType string for button styling
|
||||||
|
|
@ -324,26 +379,17 @@
|
||||||
{#if isQuirk}
|
{#if isQuirk}
|
||||||
<span>1</span>
|
<span>1</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="level-slider">
|
<Select
|
||||||
<Slider
|
options={levelOptions}
|
||||||
value={level}
|
value={level}
|
||||||
onValueChange={(v) => (level = v)}
|
onValueChange={handleLevelChange}
|
||||||
min={1}
|
contained
|
||||||
max={5}
|
/>
|
||||||
step={1}
|
|
||||||
element={elementType}
|
|
||||||
/>
|
|
||||||
<span class="level-value">{level}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
|
|
||||||
<DetailRow label="Nickname" noHover>
|
<DetailRow label="Nickname" noHover>
|
||||||
<Input
|
<Input bind:value={nickname} placeholder="Optional nickname" maxLength={50} />
|
||||||
bind:value={nickname}
|
|
||||||
placeholder="Optional nickname"
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
</DetailsSection>
|
</DetailsSection>
|
||||||
|
|
||||||
|
|
@ -354,6 +400,8 @@
|
||||||
<ArtifactSkillRow
|
<ArtifactSkillRow
|
||||||
{slot}
|
{slot}
|
||||||
skill={skills[slot - 1] ?? null}
|
skill={skills[slot - 1] ?? null}
|
||||||
|
allSkills={skills}
|
||||||
|
artifactLevel={level}
|
||||||
availableSkills={getSkillsForSlot(slot)}
|
availableSkills={getSkillsForSlot(slot)}
|
||||||
onSelectModifier={() => handleSelectModifier(slot)}
|
onSelectModifier={() => handleSelectModifier(slot)}
|
||||||
onUpdateSkill={(update) => handleUpdateSkill(slot, update)}
|
onUpdateSkill={(update) => handleUpdateSkill(slot, update)}
|
||||||
|
|
@ -364,7 +412,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -404,24 +451,9 @@
|
||||||
color: colors.$error;
|
color: colors.$error;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-slider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: spacing.$unit;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.level-value {
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
min-width: spacing.$unit-2x;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skills-list {
|
.skills-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: spacing.$unit;
|
gap: spacing.$unit;
|
||||||
padding: 0 spacing.$unit;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue