add detail and edit panes for weapon/summon collections
- WeaponEditPane: edit component with uncap, transcendence, element, weapon keys, AX skills, and awakening support - SummonEditPane: simple edit component with uncap and transcendence - CollectionWeaponPane: full detail pane with Info/My Collection tabs - CollectionSummonPane: full detail pane with Info/My Collection tabs
This commit is contained in:
parent
033bc1c8f7
commit
60947a7911
4 changed files with 1092 additions and 0 deletions
222
src/lib/components/collection/CollectionSummonPane.svelte
Normal file
222
src/lib/components/collection/CollectionSummonPane.svelte
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* CollectionSummonPane - Details and edit pane for collection summons
|
||||
*
|
||||
* Displays summon information with two views:
|
||||
* - "Info" tab: Shows base summon stats, call effect, etc.
|
||||
* - "My Collection" tab: Shows user's customizations (uncap, transcendence)
|
||||
*
|
||||
* The "My Collection" tab includes an edit mode using SummonEditPane.
|
||||
*/
|
||||
import type { CollectionSummon } from '$lib/types/api/collection'
|
||||
import { useUpdateCollectionSummon } from '$lib/api/mutations/collection.mutations'
|
||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||
import ItemHeader from '$lib/components/sidebar/details/ItemHeader.svelte'
|
||||
import BasicInfoSection from '$lib/components/sidebar/details/BasicInfoSection.svelte'
|
||||
import StatsSection from '$lib/components/sidebar/details/StatsSection.svelte'
|
||||
import SummonEditPane, {
|
||||
type SummonEditValues,
|
||||
type SummonEditUpdates
|
||||
} from '$lib/components/collection/SummonEditPane.svelte'
|
||||
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
summon: CollectionSummon
|
||||
isOwner: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
let { summon: initialSummon, isOwner, onClose }: Props = $props()
|
||||
|
||||
// Local state for the summon - updated when mutation succeeds
|
||||
let summon = $state<CollectionSummon>(initialSummon)
|
||||
|
||||
// Tab state
|
||||
let selectedTab = $state<'info' | 'collection'>('collection')
|
||||
|
||||
// Edit mode state
|
||||
let isEditing = $state(false)
|
||||
|
||||
// Sync local state when a different summon is selected
|
||||
$effect(() => {
|
||||
summon = initialSummon
|
||||
isEditing = false
|
||||
})
|
||||
|
||||
// Update mutation
|
||||
const updateMutation = useUpdateCollectionSummon()
|
||||
|
||||
// Derived values
|
||||
const summonData = $derived(summon.summon)
|
||||
|
||||
// Current edit values
|
||||
const currentValues = $derived<SummonEditValues>({
|
||||
uncapLevel: summon.uncapLevel,
|
||||
transcendenceStep: summon.transcendenceStep
|
||||
})
|
||||
|
||||
// Element name for theming
|
||||
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
const elementName = $derived(summonData?.element ? ELEMENT_MAP[summonData.element] : undefined)
|
||||
|
||||
async function handleSave(updates: SummonEditUpdates) {
|
||||
try {
|
||||
const input: Record<string, unknown> = {}
|
||||
|
||||
if (updates.uncapLevel !== undefined) {
|
||||
input.uncapLevel = updates.uncapLevel
|
||||
}
|
||||
|
||||
if (updates.transcendenceStep !== undefined) {
|
||||
input.transcendenceStep = updates.transcendenceStep
|
||||
}
|
||||
|
||||
const updatedSummon = await updateMutation.mutateAsync({
|
||||
id: summon.id,
|
||||
input
|
||||
})
|
||||
|
||||
// Update local state with the response
|
||||
summon = updatedSummon
|
||||
|
||||
isEditing = false
|
||||
} catch (error) {
|
||||
console.error('Failed to update collection summon:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
function handleTabChange(value: string) {
|
||||
selectedTab = value as 'info' | 'collection'
|
||||
if (isEditing) {
|
||||
isEditing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update sidebar header action
|
||||
$effect(() => {
|
||||
if (isOwner && selectedTab === 'collection' && !isEditing) {
|
||||
sidebar.setAction(() => (isEditing = true), 'Edit', elementName)
|
||||
} else {
|
||||
sidebar.clearAction()
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up sidebar action when component is destroyed
|
||||
$effect(() => {
|
||||
return () => {
|
||||
sidebar.clearAction()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="collection-summon-pane">
|
||||
<ItemHeader
|
||||
type="summon"
|
||||
item={summon as any}
|
||||
itemData={summonData}
|
||||
gridUncapLevel={summon.uncapLevel}
|
||||
gridTranscendence={summon.transcendenceStep}
|
||||
/>
|
||||
|
||||
<div class="tab-nav">
|
||||
<SegmentedControl
|
||||
value={selectedTab}
|
||||
onValueChange={handleTabChange}
|
||||
variant="background"
|
||||
size="small"
|
||||
grow
|
||||
>
|
||||
<Segment value="info">Info</Segment>
|
||||
<Segment value="collection">My Collection</Segment>
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
|
||||
<div class="pane-content">
|
||||
{#if selectedTab === 'info'}
|
||||
<div class="info-view">
|
||||
<BasicInfoSection type="summon" itemData={summonData} />
|
||||
<StatsSection
|
||||
itemData={summonData}
|
||||
gridUncapLevel={summon.uncapLevel}
|
||||
gridTranscendence={summon.transcendenceStep}
|
||||
/>
|
||||
</div>
|
||||
{:else if isEditing}
|
||||
<SummonEditPane
|
||||
{summonData}
|
||||
{currentValues}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
saving={updateMutation.isPending}
|
||||
/>
|
||||
{:else}
|
||||
<div class="collection-view">
|
||||
<DetailsSection title="General">
|
||||
<DetailRow label="Uncap Level">
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
uncapLevel={summon.uncapLevel}
|
||||
transcendenceStage={summon.transcendenceStep}
|
||||
flb={summonData?.uncap?.flb}
|
||||
ulb={summonData?.uncap?.ulb}
|
||||
transcendence={summonData?.uncap?.transcendence}
|
||||
/>
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.collection-summon-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
padding: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-4x;
|
||||
padding: 0 spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.collection-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-3x;
|
||||
}
|
||||
|
||||
</style>
|
||||
330
src/lib/components/collection/CollectionWeaponPane.svelte
Normal file
330
src/lib/components/collection/CollectionWeaponPane.svelte
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* CollectionWeaponPane - Details and edit pane for collection weapons
|
||||
*
|
||||
* Displays weapon information with two views:
|
||||
* - "Info" tab: Shows base weapon stats, skills, etc.
|
||||
* - "My Collection" tab: Shows user's customizations (element, keys, AX, awakening)
|
||||
*
|
||||
* The "My Collection" tab includes an edit mode using WeaponEditPane.
|
||||
*/
|
||||
import type { CollectionWeapon } from '$lib/types/api/collection'
|
||||
import type { SimpleAxSkill } from '$lib/types/api/entities'
|
||||
import { useUpdateCollectionWeapon } from '$lib/api/mutations/collection.mutations'
|
||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||
import ItemHeader from '$lib/components/sidebar/details/ItemHeader.svelte'
|
||||
import BasicInfoSection from '$lib/components/sidebar/details/BasicInfoSection.svelte'
|
||||
import StatsSection from '$lib/components/sidebar/details/StatsSection.svelte'
|
||||
import SkillsSection from '$lib/components/sidebar/details/SkillsSection.svelte'
|
||||
import WeaponEditPane, {
|
||||
type WeaponEditValues,
|
||||
type WeaponEditUpdates
|
||||
} from '$lib/components/collection/WeaponEditPane.svelte'
|
||||
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||
|
||||
interface Props {
|
||||
weapon: CollectionWeapon
|
||||
isOwner: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
let { weapon: initialWeapon, isOwner, onClose }: Props = $props()
|
||||
|
||||
// Local state for the weapon - updated when mutation succeeds
|
||||
let weapon = $state<CollectionWeapon>(initialWeapon)
|
||||
|
||||
// Tab state
|
||||
let selectedTab = $state<'info' | 'collection'>('collection')
|
||||
|
||||
// Edit mode state
|
||||
let isEditing = $state(false)
|
||||
|
||||
// Sync local state when a different weapon is selected
|
||||
$effect(() => {
|
||||
weapon = initialWeapon
|
||||
isEditing = false
|
||||
})
|
||||
|
||||
// Update mutation
|
||||
const updateMutation = useUpdateCollectionWeapon()
|
||||
|
||||
// Derived values
|
||||
const weaponData = $derived(weapon.weapon)
|
||||
|
||||
// Show instance element for element-changeable, otherwise show weapon's base element
|
||||
const displayElement = $derived(
|
||||
weaponData?.element === 0 ? weapon.element : weaponData?.element
|
||||
)
|
||||
|
||||
// Current edit values from the collection weapon
|
||||
const currentValues = $derived<WeaponEditValues>({
|
||||
uncapLevel: weapon.uncapLevel,
|
||||
transcendenceStep: weapon.transcendenceStep,
|
||||
element: weapon.element,
|
||||
weaponKey1Id: weapon.weaponKeys?.[0]?.id,
|
||||
weaponKey2Id: weapon.weaponKeys?.[1]?.id,
|
||||
weaponKey3Id: weapon.weaponKeys?.[2]?.id,
|
||||
awakening: weapon.awakening
|
||||
? {
|
||||
type: weapon.awakening.type,
|
||||
level: weapon.awakening.level
|
||||
}
|
||||
: null,
|
||||
axSkills: (weapon.ax as SimpleAxSkill[]) ?? []
|
||||
})
|
||||
|
||||
// Element name for theming
|
||||
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
const elementName = $derived(displayElement ? ELEMENT_MAP[displayElement] : undefined)
|
||||
|
||||
async function handleSave(updates: WeaponEditUpdates) {
|
||||
try {
|
||||
// Transform updates to API format
|
||||
const input: Record<string, unknown> = {}
|
||||
|
||||
if (updates.uncapLevel !== undefined) {
|
||||
input.uncapLevel = updates.uncapLevel
|
||||
}
|
||||
|
||||
if (updates.transcendenceStep !== undefined) {
|
||||
input.transcendenceStep = updates.transcendenceStep
|
||||
}
|
||||
|
||||
if (updates.element !== undefined) {
|
||||
input.element = updates.element
|
||||
}
|
||||
|
||||
// Weapon keys
|
||||
if (updates.weaponKey1Id !== undefined) {
|
||||
input.weaponKey1Id = updates.weaponKey1Id
|
||||
}
|
||||
if (updates.weaponKey2Id !== undefined) {
|
||||
input.weaponKey2Id = updates.weaponKey2Id
|
||||
}
|
||||
if (updates.weaponKey3Id !== undefined) {
|
||||
input.weaponKey3Id = updates.weaponKey3Id
|
||||
}
|
||||
|
||||
// Awakening
|
||||
if (updates.awakening !== undefined) {
|
||||
if (updates.awakening === null) {
|
||||
input.awakeningId = null
|
||||
input.awakeningLevel = null
|
||||
} else {
|
||||
input.awakeningId = updates.awakening.id
|
||||
input.awakeningLevel = updates.awakening.level
|
||||
}
|
||||
}
|
||||
|
||||
// AX skills
|
||||
if (updates.axModifier1 !== undefined) {
|
||||
input.axModifier1 = updates.axModifier1
|
||||
input.axStrength1 = updates.axStrength1
|
||||
}
|
||||
if (updates.axModifier2 !== undefined) {
|
||||
input.axModifier2 = updates.axModifier2
|
||||
input.axStrength2 = updates.axStrength2
|
||||
}
|
||||
|
||||
const updatedWeapon = await updateMutation.mutateAsync({
|
||||
id: weapon.id,
|
||||
input
|
||||
})
|
||||
|
||||
// Update local state with the response
|
||||
weapon = updatedWeapon
|
||||
|
||||
isEditing = false
|
||||
} catch (error) {
|
||||
console.error('Failed to update collection weapon:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
function handleTabChange(value: string) {
|
||||
selectedTab = value as 'info' | 'collection'
|
||||
if (isEditing) {
|
||||
isEditing = false
|
||||
}
|
||||
}
|
||||
|
||||
function getAwakeningType(): string {
|
||||
if (!weapon.awakening) return '—'
|
||||
const name =
|
||||
typeof weapon.awakening.type.name === 'string'
|
||||
? weapon.awakening.type.name
|
||||
: weapon.awakening.type.name?.en || 'Unknown'
|
||||
return name
|
||||
}
|
||||
|
||||
function getAwakeningLevel(): string {
|
||||
if (!weapon.awakening) return '—'
|
||||
return String(weapon.awakening.level)
|
||||
}
|
||||
|
||||
function getWeaponKeyName(index: number): string {
|
||||
const key = weapon.weaponKeys?.[index]
|
||||
if (!key) return '—'
|
||||
const name = key.name
|
||||
if (typeof name === 'string') return name
|
||||
return name?.en || name?.ja || '—'
|
||||
}
|
||||
|
||||
// Check conditions
|
||||
const hasAwakening = $derived(weapon.awakening !== null)
|
||||
const hasWeaponKeys = $derived((weapon.weaponKeys?.length ?? 0) > 0)
|
||||
const hasAxSkills = $derived((weapon.ax?.length ?? 0) > 0 && weapon.ax?.some(ax => ax.modifier >= 0))
|
||||
const canChangeElement = $derived(weaponData?.element === 0)
|
||||
|
||||
// Update sidebar header action
|
||||
$effect(() => {
|
||||
if (isOwner && selectedTab === 'collection' && !isEditing) {
|
||||
sidebar.setAction(() => (isEditing = true), 'Edit', elementName)
|
||||
} else {
|
||||
sidebar.clearAction()
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up sidebar action when component is destroyed
|
||||
$effect(() => {
|
||||
return () => {
|
||||
sidebar.clearAction()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="collection-weapon-pane">
|
||||
<ItemHeader
|
||||
type="weapon"
|
||||
item={weapon as any}
|
||||
itemData={weaponData}
|
||||
gridUncapLevel={weapon.uncapLevel}
|
||||
gridTranscendence={weapon.transcendenceStep}
|
||||
/>
|
||||
|
||||
<div class="tab-nav">
|
||||
<SegmentedControl
|
||||
value={selectedTab}
|
||||
onValueChange={handleTabChange}
|
||||
variant="background"
|
||||
size="small"
|
||||
grow
|
||||
>
|
||||
<Segment value="info">Info</Segment>
|
||||
<Segment value="collection">My Collection</Segment>
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
|
||||
<div class="pane-content">
|
||||
{#if selectedTab === 'info'}
|
||||
<div class="info-view">
|
||||
<BasicInfoSection type="weapon" itemData={weaponData} />
|
||||
<StatsSection
|
||||
itemData={weaponData}
|
||||
gridUncapLevel={weapon.uncapLevel}
|
||||
gridTranscendence={weapon.transcendenceStep}
|
||||
/>
|
||||
<SkillsSection type="weapon" itemData={weaponData} />
|
||||
</div>
|
||||
{:else if isEditing}
|
||||
<WeaponEditPane
|
||||
{weaponData}
|
||||
{currentValues}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
saving={updateMutation.isPending}
|
||||
/>
|
||||
{:else}
|
||||
<div class="collection-view">
|
||||
<DetailsSection title="General">
|
||||
<DetailRow label="Uncap Level">
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
uncapLevel={weapon.uncapLevel}
|
||||
transcendenceStage={weapon.transcendenceStep}
|
||||
flb={weaponData?.uncap?.flb}
|
||||
ulb={weaponData?.uncap?.ulb}
|
||||
transcendence={weaponData?.uncap?.transcendence}
|
||||
/>
|
||||
</DetailRow>
|
||||
{#if canChangeElement}
|
||||
<DetailRow label="Element">
|
||||
<ElementLabel element={displayElement} size="medium" />
|
||||
</DetailRow>
|
||||
{/if}
|
||||
</DetailsSection>
|
||||
|
||||
<DetailsSection title="Awakening" empty={!hasAwakening} emptyMessage="Not set">
|
||||
<DetailRow label="Type" value={getAwakeningType()} />
|
||||
<DetailRow label="Level" value={getAwakeningLevel()} />
|
||||
</DetailsSection>
|
||||
|
||||
<DetailsSection title="Weapon Keys" empty={!hasWeaponKeys} emptyMessage="Not set">
|
||||
{#each weapon.weaponKeys ?? [] as key, i}
|
||||
<DetailRow label="Key {i + 1}" value={getWeaponKeyName(i)} />
|
||||
{/each}
|
||||
</DetailsSection>
|
||||
|
||||
<DetailsSection title="AX Skills" empty={!hasAxSkills} emptyMessage="Not set">
|
||||
{#each weapon.ax ?? [] as ax, i}
|
||||
{#if ax.modifier >= 0}
|
||||
<DetailRow label="Skill {i + 1}" value={`${ax.modifier}: ${ax.strength}`} />
|
||||
{/if}
|
||||
{/each}
|
||||
</DetailsSection>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.collection-weapon-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
padding: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-4x;
|
||||
padding: 0 spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.collection-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-3x;
|
||||
}
|
||||
</style>
|
||||
152
src/lib/components/collection/SummonEditPane.svelte
Normal file
152
src/lib/components/collection/SummonEditPane.svelte
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* SummonEditPane - Edit component for collection summons
|
||||
*
|
||||
* Provides edit controls for summon customization:
|
||||
* - Uncap level (editable UncapIndicator)
|
||||
* - Transcendence step
|
||||
*
|
||||
* Summons are simpler than weapons/characters - they only track uncap and transcendence.
|
||||
*/
|
||||
import type { Summon } from '$lib/types/api/entities'
|
||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
export interface SummonEditValues {
|
||||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
}
|
||||
|
||||
export interface SummonEditUpdates {
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** The base summon data */
|
||||
summonData: Summon | undefined
|
||||
/** Current values for all edit fields */
|
||||
currentValues: SummonEditValues
|
||||
/** Callback when save is clicked */
|
||||
onSave?: (updates: SummonEditUpdates) => void
|
||||
/** Callback when cancel is clicked */
|
||||
onCancel?: () => void
|
||||
/** Whether save is in progress */
|
||||
saving?: boolean
|
||||
}
|
||||
|
||||
let { summonData, currentValues, onSave, onCancel, saving = false }: Props = $props()
|
||||
|
||||
// Internal state
|
||||
let uncapLevel = $state(currentValues.uncapLevel)
|
||||
let transcendenceStep = $state(currentValues.transcendenceStep)
|
||||
|
||||
// Re-initialize when currentValues changes
|
||||
$effect(() => {
|
||||
uncapLevel = currentValues.uncapLevel
|
||||
transcendenceStep = currentValues.transcendenceStep
|
||||
})
|
||||
|
||||
// Element name for theming
|
||||
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
const elementName = $derived(summonData?.element ? ELEMENT_MAP[summonData.element] : undefined)
|
||||
|
||||
function handleUncapUpdate(newLevel: number) {
|
||||
uncapLevel = newLevel
|
||||
}
|
||||
|
||||
function handleTranscendenceUpdate(newStage: number) {
|
||||
transcendenceStep = newStage
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const updates: SummonEditUpdates = {
|
||||
uncapLevel,
|
||||
transcendenceStep
|
||||
}
|
||||
onSave?.(updates)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset to original values
|
||||
uncapLevel = currentValues.uncapLevel
|
||||
transcendenceStep = currentValues.transcendenceStep
|
||||
onCancel?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="summon-edit-pane">
|
||||
<div class="edit-sections">
|
||||
<DetailsSection title="Uncap Level">
|
||||
<div class="section-content uncap-section">
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
{uncapLevel}
|
||||
transcendenceStage={transcendenceStep}
|
||||
flb={summonData?.uncap?.flb}
|
||||
ulb={summonData?.uncap?.ulb}
|
||||
transcendence={summonData?.uncap?.transcendence}
|
||||
editable={true}
|
||||
updateUncap={handleUncapUpdate}
|
||||
updateTranscendence={handleTranscendenceUpdate}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
</div>
|
||||
|
||||
<div class="edit-footer">
|
||||
<Button variant="secondary" onclick={handleCancel} disabled={saving}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
element={elementName}
|
||||
elementStyle={!!elementName}
|
||||
onclick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
|
||||
.summon-edit-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.edit-sections {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-3x;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: spacing.$unit;
|
||||
}
|
||||
|
||||
.edit-footer {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
padding: spacing.$unit-2x;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
388
src/lib/components/collection/WeaponEditPane.svelte
Normal file
388
src/lib/components/collection/WeaponEditPane.svelte
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* WeaponEditPane - Edit component for collection weapons
|
||||
*
|
||||
* Provides edit controls for weapon customization:
|
||||
* - Uncap level (editable UncapIndicator)
|
||||
* - Transcendence step
|
||||
* - Element (for element-changeable weapons)
|
||||
* - Weapon keys (for Opus, Ultima, Draconic, Astral, Superlative)
|
||||
* - AX skills (for weapons with AX support)
|
||||
* - Awakening (for weapons with awakening support)
|
||||
*/
|
||||
import type { Weapon, Awakening, SimpleAxSkill } from '$lib/types/api/entities'
|
||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||
import Select from '$lib/components/ui/Select.svelte'
|
||||
import WeaponKeySelect from '$lib/components/sidebar/edit/WeaponKeySelect.svelte'
|
||||
import AwakeningSelect from '$lib/components/sidebar/edit/AwakeningSelect.svelte'
|
||||
import AxSkillSelect from '$lib/components/sidebar/edit/AxSkillSelect.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import { getElementIcon } from '$lib/utils/images'
|
||||
|
||||
export interface WeaponEditValues {
|
||||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
element?: number
|
||||
weaponKey1Id?: string
|
||||
weaponKey2Id?: string
|
||||
weaponKey3Id?: string
|
||||
awakening?: {
|
||||
type?: Awakening
|
||||
level: number
|
||||
} | null
|
||||
axSkills: SimpleAxSkill[]
|
||||
}
|
||||
|
||||
export interface WeaponEditUpdates {
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
element?: number
|
||||
weaponKey1Id?: string
|
||||
weaponKey2Id?: string
|
||||
weaponKey3Id?: string
|
||||
weaponKey4Id?: string
|
||||
awakening?: {
|
||||
id: string
|
||||
level: number
|
||||
} | null
|
||||
axModifier1?: number
|
||||
axStrength1?: number
|
||||
axModifier2?: number
|
||||
axStrength2?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** The base weapon data */
|
||||
weaponData: Weapon | undefined
|
||||
/** Current values for all edit fields */
|
||||
currentValues: WeaponEditValues
|
||||
/** Callback when save is clicked */
|
||||
onSave?: (updates: WeaponEditUpdates) => void
|
||||
/** Callback when cancel is clicked */
|
||||
onCancel?: () => void
|
||||
/** Whether save is in progress */
|
||||
saving?: boolean
|
||||
}
|
||||
|
||||
let { weaponData, currentValues, onSave, onCancel, saving = false }: Props = $props()
|
||||
|
||||
// Internal state
|
||||
let uncapLevel = $state(currentValues.uncapLevel)
|
||||
let transcendenceStep = $state(currentValues.transcendenceStep)
|
||||
let element = $state(currentValues.element ?? weaponData?.element ?? 0)
|
||||
let weaponKey1 = $state<string | undefined>(currentValues.weaponKey1Id)
|
||||
let weaponKey2 = $state<string | undefined>(currentValues.weaponKey2Id)
|
||||
let weaponKey3 = $state<string | undefined>(currentValues.weaponKey3Id)
|
||||
let selectedAwakening = $state<Awakening | undefined>(currentValues.awakening?.type)
|
||||
let awakeningLevel = $state(currentValues.awakening?.level ?? 1)
|
||||
let axSkills = $state<SimpleAxSkill[]>(
|
||||
currentValues.axSkills.length > 0
|
||||
? currentValues.axSkills
|
||||
: [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
)
|
||||
|
||||
// Re-initialize when currentValues changes
|
||||
$effect(() => {
|
||||
uncapLevel = currentValues.uncapLevel
|
||||
transcendenceStep = currentValues.transcendenceStep
|
||||
element = currentValues.element ?? weaponData?.element ?? 0
|
||||
weaponKey1 = currentValues.weaponKey1Id
|
||||
weaponKey2 = currentValues.weaponKey2Id
|
||||
weaponKey3 = currentValues.weaponKey3Id
|
||||
selectedAwakening = currentValues.awakening?.type
|
||||
awakeningLevel = currentValues.awakening?.level ?? 1
|
||||
axSkills =
|
||||
currentValues.axSkills.length > 0
|
||||
? currentValues.axSkills
|
||||
: [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
})
|
||||
|
||||
// Derived conditions
|
||||
const canChangeElement = $derived(weaponData?.element === 0)
|
||||
const series = $derived(weaponData?.series ?? 0)
|
||||
|
||||
// Weapon key config keyed by WEAPON series
|
||||
const WEAPON_KEY_SERIES: Record<number, { name: string; slots: number; keySeries: number }> = {
|
||||
2: { name: 'Dark Opus', slots: 2, keySeries: 3 },
|
||||
3: { name: 'Ultima', slots: 3, keySeries: 13 },
|
||||
17: { name: 'Draconic', slots: 2, keySeries: 27 },
|
||||
22: { name: 'Astral', slots: 1, keySeries: 19 },
|
||||
34: { name: 'Superlative', slots: 2, keySeries: 40 }
|
||||
}
|
||||
|
||||
const weaponKeyConfig = $derived(WEAPON_KEY_SERIES[series])
|
||||
const hasWeaponKeys = $derived(!!weaponKeyConfig)
|
||||
const keySlotCount = $derived(weaponKeyConfig?.slots ?? 0)
|
||||
const keySeries = $derived(weaponKeyConfig?.keySeries ?? 0)
|
||||
|
||||
const hasAxSkills = $derived(weaponData?.ax === true)
|
||||
const axType = $derived(weaponData?.axType ?? 1)
|
||||
const hasAwakening = $derived((weaponData?.maxAwakeningLevel ?? 0) > 0)
|
||||
const availableAwakenings = $derived(weaponData?.awakenings ?? [])
|
||||
|
||||
// Element name for theming
|
||||
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||
1: 'wind',
|
||||
2: 'fire',
|
||||
3: 'water',
|
||||
4: 'earth',
|
||||
5: 'dark',
|
||||
6: 'light'
|
||||
}
|
||||
const weaponElement = $derived(element || weaponData?.element)
|
||||
const elementName = $derived(weaponElement ? ELEMENT_MAP[weaponElement] : undefined)
|
||||
|
||||
// Element options
|
||||
const elementOptions = [
|
||||
{ value: 1, label: 'Wind', image: getElementIcon(1) },
|
||||
{ value: 2, label: 'Fire', image: getElementIcon(2) },
|
||||
{ value: 3, label: 'Water', image: getElementIcon(3) },
|
||||
{ value: 4, label: 'Earth', image: getElementIcon(4) },
|
||||
{ value: 5, label: 'Dark', image: getElementIcon(5) },
|
||||
{ value: 6, label: 'Light', image: getElementIcon(6) }
|
||||
]
|
||||
|
||||
// Awakening slug to UUID map
|
||||
const AWAKENING_MAP: Record<string, string> = {
|
||||
'weapon-balanced': 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'weapon-atk': 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
|
||||
'weapon-def': 'c3d4e5f6-a7b8-9012-cdef-123456789012',
|
||||
'weapon-multi': 'd4e5f6a7-b8c9-0123-def0-234567890123'
|
||||
}
|
||||
|
||||
function handleUncapUpdate(newLevel: number) {
|
||||
uncapLevel = newLevel
|
||||
}
|
||||
|
||||
function handleTranscendenceUpdate(newStage: number) {
|
||||
transcendenceStep = newStage
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const updates: WeaponEditUpdates = {
|
||||
uncapLevel,
|
||||
transcendenceStep
|
||||
}
|
||||
|
||||
// Element for element-changeable weapons
|
||||
if (canChangeElement) {
|
||||
updates.element = element
|
||||
}
|
||||
|
||||
// Weapon keys
|
||||
if (hasWeaponKeys) {
|
||||
if (weaponKey1) updates.weaponKey1Id = weaponKey1
|
||||
if (weaponKey2) updates.weaponKey2Id = weaponKey2
|
||||
if (weaponKey3) updates.weaponKey3Id = weaponKey3
|
||||
}
|
||||
|
||||
// Awakening
|
||||
if (hasAwakening) {
|
||||
if (selectedAwakening?.slug) {
|
||||
const awakeningId = AWAKENING_MAP[selectedAwakening.slug]
|
||||
if (awakeningId) {
|
||||
updates.awakening = {
|
||||
id: awakeningId,
|
||||
level: awakeningLevel
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updates.awakening = null
|
||||
}
|
||||
}
|
||||
|
||||
// AX Skills
|
||||
if (hasAxSkills && axSkills.length >= 2) {
|
||||
if (axSkills[0] && axSkills[0].modifier >= 0) {
|
||||
updates.axModifier1 = axSkills[0].modifier
|
||||
updates.axStrength1 = axSkills[0].strength
|
||||
}
|
||||
if (axSkills[1] && axSkills[1].modifier >= 0) {
|
||||
updates.axModifier2 = axSkills[1].modifier
|
||||
updates.axStrength2 = axSkills[1].strength
|
||||
}
|
||||
}
|
||||
|
||||
onSave?.(updates)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset to original values
|
||||
uncapLevel = currentValues.uncapLevel
|
||||
transcendenceStep = currentValues.transcendenceStep
|
||||
element = currentValues.element ?? weaponData?.element ?? 0
|
||||
weaponKey1 = currentValues.weaponKey1Id
|
||||
weaponKey2 = currentValues.weaponKey2Id
|
||||
weaponKey3 = currentValues.weaponKey3Id
|
||||
selectedAwakening = currentValues.awakening?.type
|
||||
awakeningLevel = currentValues.awakening?.level ?? 1
|
||||
axSkills =
|
||||
currentValues.axSkills.length > 0
|
||||
? currentValues.axSkills
|
||||
: [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
onCancel?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="weapon-edit-pane">
|
||||
<div class="edit-sections">
|
||||
<DetailsSection title="Uncap Level">
|
||||
<div class="section-content uncap-section">
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
{uncapLevel}
|
||||
transcendenceStage={transcendenceStep}
|
||||
flb={weaponData?.uncap?.flb}
|
||||
ulb={weaponData?.uncap?.ulb}
|
||||
transcendence={weaponData?.uncap?.transcendence}
|
||||
editable={true}
|
||||
updateUncap={handleUncapUpdate}
|
||||
updateTranscendence={handleTranscendenceUpdate}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
|
||||
{#if canChangeElement}
|
||||
<DetailsSection title="Element">
|
||||
<div class="section-content">
|
||||
<Select
|
||||
options={elementOptions}
|
||||
bind:value={element}
|
||||
placeholder="Select element"
|
||||
size="medium"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasWeaponKeys}
|
||||
<DetailsSection title="Weapon Keys">
|
||||
<div class="section-content key-selects">
|
||||
{#if keySlotCount >= 1}
|
||||
<WeaponKeySelect
|
||||
series={keySeries}
|
||||
slot={0}
|
||||
bind:value={weaponKey1}
|
||||
{transcendenceStep}
|
||||
/>
|
||||
{/if}
|
||||
{#if keySlotCount >= 2}
|
||||
<WeaponKeySelect
|
||||
series={keySeries}
|
||||
slot={1}
|
||||
bind:value={weaponKey2}
|
||||
{transcendenceStep}
|
||||
/>
|
||||
{/if}
|
||||
{#if keySlotCount >= 3}
|
||||
<WeaponKeySelect
|
||||
series={keySeries}
|
||||
slot={2}
|
||||
bind:value={weaponKey3}
|
||||
{transcendenceStep}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasAxSkills}
|
||||
<DetailsSection title="AX Skills">
|
||||
<div class="section-content">
|
||||
<AxSkillSelect
|
||||
{axType}
|
||||
currentSkills={axSkills}
|
||||
onChange={(skills) => {
|
||||
axSkills = skills
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasAwakening && availableAwakenings.length > 0}
|
||||
<DetailsSection title="Awakening">
|
||||
<div class="section-content">
|
||||
<AwakeningSelect
|
||||
awakenings={availableAwakenings}
|
||||
value={selectedAwakening}
|
||||
level={awakeningLevel}
|
||||
maxLevel={weaponData?.maxAwakeningLevel ?? 9}
|
||||
onAwakeningChange={(awakening) => {
|
||||
selectedAwakening = awakening
|
||||
}}
|
||||
onLevelChange={(level) => {
|
||||
awakeningLevel = level
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="edit-footer">
|
||||
<Button variant="secondary" onclick={handleCancel} disabled={saving}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
element={elementName}
|
||||
elementStyle={!!elementName}
|
||||
onclick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
|
||||
.weapon-edit-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.edit-sections {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-3x;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: spacing.$unit;
|
||||
}
|
||||
|
||||
.key-selects {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.edit-footer {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
padding: spacing.$unit-2x;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue