From f55303039c45e3f5dd0db2664bbe5c2a6f52f271 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 20:50:53 -0800 Subject: [PATCH] auto-redistribute skill levels when artifact level changes --- .../artifact/ArtifactEditPane.svelte | 221 +++++++++++++----- .../CollectionArtifactEditPane.svelte | 48 ++-- .../sidebar/AddArtifactSidebar.svelte | 122 ++++++---- 3 files changed, 272 insertions(+), 119 deletions(-) diff --git a/src/lib/components/artifact/ArtifactEditPane.svelte b/src/lib/components/artifact/ArtifactEditPane.svelte index 120bd35c..6378699a 100644 --- a/src/lib/components/artifact/ArtifactEditPane.svelte +++ b/src/lib/components/artifact/ArtifactEditPane.svelte @@ -4,20 +4,19 @@ import type { ArtifactInstance, ArtifactSkillInstance, - ArtifactSkill, - ArtifactGrade + ArtifactSkill } from '$lib/types/api/artifact' import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact' import { createQuery } from '@tanstack/svelte-query' 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 DetailRow from '$lib/components/sidebar/details/DetailRow.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 ArtifactModifierList from './ArtifactModifierList.svelte' - import ArtifactGradeDisplay from './ArtifactGradeDisplay.svelte' + import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte' interface Props { /** The artifact instance being edited */ @@ -34,10 +33,63 @@ const paneStack = usePaneStack() // 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 level = $state(artifact.level) 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 const artifactData = $derived(artifact.artifact) @@ -66,6 +118,7 @@ ] // Convert numeric element to ElementType string + type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' const elementTypeMap: Record = { 1: 'wind', 2: 'fire', @@ -74,7 +127,7 @@ 5: 'dark', 6: 'light' } - const elementType = $derived(elementTypeMap[element] ?? undefined) + const elementType = $derived(elementTypeMap[element]) // Level options (1-5 for standard, fixed at 1 for quirk) const levelOptions = $derived( @@ -103,13 +156,6 @@ { 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 function handleSelectModifier(slot: number) { const config: PaneConfig = { @@ -119,6 +165,7 @@ props: { slot, selectedModifier: skills[slot - 1]?.modifier, + element: elementType, onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill) } } @@ -139,10 +186,10 @@ } 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() + notifyUpdate() } // Handle skill updates from skill row @@ -164,13 +211,64 @@ notifyUpdate() } - // Handle level change + // 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 increased, distribute extra points to skills + // If budget decreased, remove points from skills (starting from highest) + if (budgetDiff !== 0 && !isQuirk) { + redistributeSkillLevels(budgetDiff) + } + 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 function handleProficiencyChange(newProficiency: number | undefined) { if (newProficiency === undefined) return @@ -178,11 +276,19 @@ notifyUpdate() } + // Handle nickname change + function handleNicknameChange(event: Event) { + const target = event.target as HTMLInputElement + nickname = target.value + notifyUpdate() + } + // Notify parent of updates function notifyUpdate() { if (!onUpdate) return const updates: Partial = { + nickname: nickname.trim() || undefined, element, level, skills: [...skills] @@ -194,17 +300,28 @@ onUpdate(updates) } - - // Current grade (from artifact or could be recalculated) - const currentGrade: ArtifactGrade = $derived(artifact.grade)
- + + + {#if disabled} + {nickname || '—'} + {:else} + + {/if} + {#if canChangeProficiency} {#if disabled} - {getProficiencyName(proficiency)} + {:else} {/if} @@ -267,6 +381,8 @@ handleSelectModifier(slot)} onUpdateSkill={(update) => handleUpdateSkill(slot, update)} @@ -276,12 +392,6 @@
{/if} - - -
- -
-
diff --git a/src/lib/components/collection/CollectionArtifactEditPane.svelte b/src/lib/components/collection/CollectionArtifactEditPane.svelte index b951fb40..47a907be 100644 --- a/src/lib/components/collection/CollectionArtifactEditPane.svelte +++ b/src/lib/components/collection/CollectionArtifactEditPane.svelte @@ -16,9 +16,11 @@ interface Props { 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() @@ -49,20 +51,40 @@ function handleSave() { 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({ id: artifact.id, - 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 - } + input }, { - onSuccess: () => { + onSuccess: (updatedArtifact) => { + onSaved?.(updatedArtifact) paneStack.pop() + }, + onError: (error) => { + console.error('[CollectionArtifactEditPane] Save failed:', error) } }) } @@ -78,9 +100,9 @@ return () => sidebar.clearAction() }) - // Reactively update header when state changes + // Reactively update header when state changes or when returning from sub-pane $effect(() => { - const _ = [hasChanges, updateMutation.isPending] + const _ = [hasChanges, updateMutation.isPending, paneStack.panes.length] untrack(() => updateHeader()) }) diff --git a/src/lib/components/sidebar/AddArtifactSidebar.svelte b/src/lib/components/sidebar/AddArtifactSidebar.svelte index 2dac0669..dc784546 100644 --- a/src/lib/components/sidebar/AddArtifactSidebar.svelte +++ b/src/lib/components/sidebar/AddArtifactSidebar.svelte @@ -12,7 +12,12 @@ * 5. Configure skills (for standard artifacts only, uses pane stack) */ 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 { createQuery } from '@tanstack/svelte-query' import { artifactQueries } from '$lib/api/queries/artifact.queries' @@ -22,7 +27,6 @@ import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte' import DetailRow from '$lib/components/sidebar/details/DetailRow.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 '$lib/components/artifact/ArtifactSkillRow.svelte' import ArtifactModifierList from '$lib/components/artifact/ArtifactModifierList.svelte' @@ -63,7 +67,7 @@ 3: '#5cb7ec', // Water - blue 4: '#ec985c', // Earth - orange/brown 5: '#c65cec', // Dark - purple - 6: '#c59c0c' // Light - gold/yellow + 6: '#c59c0c' // Light - gold/yellow } // Proficiency options - matches database enum values @@ -94,7 +98,7 @@ // Standard artifacts have a fixed proficiency, quirk artifacts match any proficiency const filteredArtifacts = $derived.by(() => { 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 if (a.proficiency === null) return true // Standard artifacts match their fixed proficiency @@ -104,16 +108,14 @@ // Build artifact options for dropdown (filtered by proficiency) const artifactOptions = $derived.by(() => { - return filteredArtifacts.map(a => ({ + return filteredArtifacts.map((a) => ({ 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 - const selectedArtifact = $derived( - artifactsQuery.data?.find(a => a.id === selectedArtifactId) - ) + const selectedArtifact = $derived(artifactsQuery.data?.find((a) => a.id === selectedArtifactId)) const isQuirk = $derived(selectedArtifact ? isQuirkArtifact(selectedArtifact) : false) // Level options (1-5 for standard, fixed at 1 for quirk) @@ -145,6 +147,7 @@ props: { slot, selectedModifier: skills[slot - 1]?.modifier, + element: elementType, onSelect: (skill: ArtifactSkill) => handleModifierSelected(slot, skill) } } @@ -195,17 +198,69 @@ // Reset skills when artifact changes skills = [null, null, null, null] // 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)) { 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 const isValid = $derived( - proficiency !== undefined && - element !== undefined && - selectedArtifactId !== undefined + proficiency !== undefined && element !== undefined && selectedArtifactId !== undefined ) // Convert numeric element to ElementType string for button styling @@ -324,26 +379,17 @@ {#if isQuirk} 1 {:else} -
- (level = v)} - min={1} - max={5} - step={1} - element={elementType} - /> - {level} -
+ + @@ -354,6 +400,8 @@ handleSelectModifier(slot)} onUpdateSkill={(update) => handleUpdateSkill(slot, update)} @@ -364,7 +412,6 @@ {/if} {/if} -