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:
Justin Edmund 2025-12-03 07:26:53 -08:00
parent 033bc1c8f7
commit 60947a7911
4 changed files with 1092 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>