diff --git a/src/lib/api/adapters/__tests__/entity.adapter.test.ts b/src/lib/api/adapters/__tests__/entity.adapter.test.ts index 89b4437c..95029209 100644 --- a/src/lib/api/adapters/__tests__/entity.adapter.test.ts +++ b/src/lib/api/adapters/__tests__/entity.adapter.test.ts @@ -29,7 +29,7 @@ describe('EntityAdapter', () => { name: { en: 'Dark Opus', ja: 'ダークオーパス' }, hasWeaponKeys: true, hasAwakening: true, - hasAxSkills: false, + augmentType: 'none', extra: false, elementChangeable: false }, diff --git a/src/lib/api/adapters/__tests__/grid.adapter.test.ts b/src/lib/api/adapters/__tests__/grid.adapter.test.ts index b86040cb..1c39aed7 100644 --- a/src/lib/api/adapters/__tests__/grid.adapter.test.ts +++ b/src/lib/api/adapters/__tests__/grid.adapter.test.ts @@ -29,7 +29,7 @@ describe('GridAdapter', () => { name: { en: 'Dark Opus', ja: 'ダークオーパス' }, hasWeaponKeys: true, hasAwakening: true, - hasAxSkills: false, + augmentType: 'none', extra: false, elementChangeable: false }, diff --git a/src/lib/api/adapters/entity.adapter.ts b/src/lib/api/adapters/entity.adapter.ts index c60c68c0..80da378c 100644 --- a/src/lib/api/adapters/entity.adapter.ts +++ b/src/lib/api/adapters/entity.adapter.ts @@ -17,6 +17,7 @@ import type { CreateWeaponSeriesPayload, UpdateWeaponSeriesPayload } from '$lib/types/api/weaponSeries' +import type { WeaponStatModifier } from '$lib/types/api/weaponStatModifier' import type { CharacterSeriesRef, CharacterSeries, @@ -731,10 +732,47 @@ export class EntityAdapter extends BaseAdapter { }) } + // ============================================ + // Weapon Stat Modifier Methods (AX Skills & Befoulments) + // ============================================ + + /** + * Gets all weapon stat modifiers (AX skills and befoulments) + * @param category - Optional filter: 'ax' or 'befoulment' + */ + async getWeaponStatModifiers(category?: 'ax' | 'befoulment'): Promise { + const searchParams = new URLSearchParams() + if (category) { + searchParams.set('category', category) + } + + const queryString = searchParams.toString() + const url = queryString ? `/weapon_stat_modifiers?${queryString}` : '/weapon_stat_modifiers' + + return this.request(url, { + method: 'GET', + cacheTTL: 3600000 // Cache for 1 hour - reference data rarely changes + }) + } + + /** + * Gets AX skills only + */ + async getAxSkills(): Promise { + return this.getWeaponStatModifiers('ax') + } + + /** + * Gets befoulments only + */ + async getBefoulments(): Promise { + return this.getWeaponStatModifiers('befoulment') + } + /** * Clears entity cache */ - clearEntityCache(type?: 'weapons' | 'characters' | 'summons' | 'weapon_keys' | 'weapon_series') { + clearEntityCache(type?: 'weapons' | 'characters' | 'summons' | 'weapon_keys' | 'weapon_series' | 'weapon_stat_modifiers') { if (type) { this.clearCache(`/${type}`) } else { @@ -744,6 +782,7 @@ export class EntityAdapter extends BaseAdapter { this.clearCache('/summons') this.clearCache('/weapon_keys') this.clearCache('/weapon_series') + this.clearCache('/weapon_stat_modifiers') } } diff --git a/src/lib/api/queries/entity.queries.ts b/src/lib/api/queries/entity.queries.ts index ce764096..b7033720 100644 --- a/src/lib/api/queries/entity.queries.ts +++ b/src/lib/api/queries/entity.queries.ts @@ -173,6 +173,46 @@ export const entityQueries = { enabled: !!idOrSlug, staleTime: 1000 * 60 * 60, // 1 hour gcTime: 1000 * 60 * 60 * 24 // 24 hours + }), + + /** + * Weapon stat modifiers query options (AX skills and befoulments) + * + * @param category - Optional filter: 'ax' or 'befoulment' + * @returns Query options for fetching weapon stat modifiers + */ + weaponStatModifiers: (category?: 'ax' | 'befoulment') => + queryOptions({ + queryKey: ['weaponStatModifiers', category ?? 'all'] as const, + queryFn: () => entityAdapter.getWeaponStatModifiers(category), + staleTime: 1000 * 60 * 60, // 1 hour - reference data + gcTime: 1000 * 60 * 60 * 24 // 24 hours + }), + + /** + * AX skills only query options + * + * @returns Query options for fetching AX skills + */ + axSkills: () => + queryOptions({ + queryKey: ['weaponStatModifiers', 'ax'] as const, + queryFn: () => entityAdapter.getAxSkills(), + staleTime: 1000 * 60 * 60, // 1 hour + gcTime: 1000 * 60 * 60 * 24 // 24 hours + }), + + /** + * Befoulments only query options + * + * @returns Query options for fetching befoulments + */ + befoulments: () => + queryOptions({ + queryKey: ['weaponStatModifiers', 'befoulment'] as const, + queryFn: () => entityAdapter.getBefoulments(), + staleTime: 1000 * 60 * 60, // 1 hour + gcTime: 1000 * 60 * 60 * 24 // 24 hours }) } @@ -210,5 +250,10 @@ export const entityKeys = { allCharacterSeries: () => ['characterSeries'] as const, summonSeriesList: () => ['summonSeries', 'list'] as const, summonSeries: (idOrSlug: string) => ['summonSeries', idOrSlug] as const, - allSummonSeries: () => ['summonSeries'] as const + allSummonSeries: () => ['summonSeries'] as const, + weaponStatModifiers: (category?: 'ax' | 'befoulment') => + ['weaponStatModifiers', category ?? 'all'] as const, + allWeaponStatModifiers: () => ['weaponStatModifiers'] as const, + axSkills: () => ['weaponStatModifiers', 'ax'] as const, + befoulments: () => ['weaponStatModifiers', 'befoulment'] as const } diff --git a/src/lib/api/schemas/party.ts b/src/lib/api/schemas/party.ts index 0f23d274..71424070 100644 --- a/src/lib/api/schemas/party.ts +++ b/src/lib/api/schemas/party.ts @@ -198,11 +198,36 @@ const WeaponSeriesRefSchema = z.object({ }), has_weapon_keys: z.boolean().optional(), has_awakening: z.boolean().optional(), - has_ax_skills: z.boolean().optional(), + augment_type: z.enum(['none', 'ax', 'befoulment']).optional(), extra: z.boolean().optional(), element_changeable: z.boolean().optional() }) +// Weapon stat modifier schema (for AX skills and befoulments) +const WeaponStatModifierSchema = z.object({ + id: z.string(), + slug: z.string(), + name_en: z.string(), + name_jp: z.string(), + category: z.enum(['ax', 'befoulment']), + stat: z.string(), + polarity: z.number(), + suffix: z.string().nullable() +}) + +// AX skill with modifier object and strength +const AugmentSkillSchema = z.object({ + modifier: WeaponStatModifierSchema, + strength: z.number() +}) + +// Befoulment with modifier, strength, and exorcism level +const BefoulmentSchema = z.object({ + modifier: WeaponStatModifierSchema, + strength: z.number(), + exorcism_level: z.number() +}) + // Item schemas const WeaponSchema = z.object({ id: z.string(), @@ -282,28 +307,33 @@ const GridWeaponSchema = z.object({ transcendence_step: z.number().nullish().default(0), transcendence_level: z.number().nullish().default(0), // Alias for compatibility element: z.number().nullish(), - + // Weapon keys weapon_key1_id: z.string().nullish(), weapon_key2_id: z.string().nullish(), weapon_key3_id: z.string().nullish(), weapon_key4_id: z.string().nullish(), weapon_keys: z.array(z.any()).nullish(), // Populated by API with key details - + // Awakening awakening_id: z.string().nullish(), awakening_level: z.number().nullish().default(1), awakening: z.any().nullish(), // Populated by API with awakening details - - // AX modifiers - ax_modifier1: z.number().nullish(), - ax_strength1: z.number().nullish(), - ax_modifier2: z.number().nullish(), - ax_strength2: z.number().nullish(), - + + // AX skills (new format with full modifier objects) + ax: z.array(AugmentSkillSchema).nullish(), + + // Befoulment (for Odiant weapons) + befoulment: BefoulmentSchema.nullish(), + // Nested weapon data (populated by API) weapon: WeaponSchema.nullish(), - + + // Collection link fields + collection_weapon_id: z.string().nullish(), + out_of_sync: z.boolean().nullish(), + orphaned: z.boolean().nullish(), + created_at: z.string().nullish(), updated_at: z.string().nullish() }) diff --git a/src/lib/components/collection/CollectionWeaponPane.svelte b/src/lib/components/collection/CollectionWeaponPane.svelte index 3ce6f37c..59ca3b2f 100644 --- a/src/lib/components/collection/CollectionWeaponPane.svelte +++ b/src/lib/components/collection/CollectionWeaponPane.svelte @@ -10,7 +10,7 @@ */ import { onMount } from 'svelte' import type { CollectionWeapon } from '$lib/types/api/collection' - import type { SimpleAxSkill } from '$lib/types/api/entities' + import type { AugmentSkill, Befoulment } from '$lib/types/api/weaponStatModifier' import { useUpdateCollectionWeapon, useRemoveWeaponFromCollection @@ -89,7 +89,8 @@ level: weapon.awakening.level } : null, - axSkills: (weapon.ax as SimpleAxSkill[]) ?? [] + axSkills: (weapon.ax as AugmentSkill[]) ?? [], + befoulment: (weapon.befoulment as Befoulment) ?? null }) // Element name for theming @@ -143,15 +144,22 @@ } // AX skills - if (updates.axModifier1 !== undefined) { - input.axModifier1 = updates.axModifier1 + if (updates.axModifier1Id !== undefined) { + input.axModifier1Id = updates.axModifier1Id input.axStrength1 = updates.axStrength1 } - if (updates.axModifier2 !== undefined) { - input.axModifier2 = updates.axModifier2 + if (updates.axModifier2Id !== undefined) { + input.axModifier2Id = updates.axModifier2Id input.axStrength2 = updates.axStrength2 } + // Befoulment + if (updates.befoulmentModifierId !== undefined) { + input.befoulmentModifierId = updates.befoulmentModifierId + input.befoulmentStrength = updates.befoulmentStrength + input.exorcismLevel = updates.exorcismLevel + } + const updatedWeapon = await updateMutation.mutateAsync({ id: weapon.id, input @@ -247,7 +255,7 @@ // 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 hasAxSkills = $derived((weapon.ax?.length ?? 0) > 0 && weapon.ax?.some(ax => ax.modifier?.id)) const canChangeElement = $derived(weaponData?.element === 0) // Set up sidebar action on mount and clean up on destroy @@ -334,8 +342,8 @@ {#each weapon.ax ?? [] as ax, i} - {#if ax.modifier >= 0} - + {#if ax.modifier?.id} + {/if} {/each} diff --git a/src/lib/components/collection/WeaponEditPane.svelte b/src/lib/components/collection/WeaponEditPane.svelte index b647e295..0ffa714c 100644 --- a/src/lib/components/collection/WeaponEditPane.svelte +++ b/src/lib/components/collection/WeaponEditPane.svelte @@ -10,12 +10,14 @@ * - AX skills (for weapons with AX support) * - Awakening (for weapons with awakening support) */ - import type { Weapon, Awakening, SimpleAxSkill } from '$lib/types/api/entities' + import type { Weapon, Awakening } from '$lib/types/api/entities' + import type { AugmentSkill, Befoulment } from '$lib/types/api/weaponStatModifier' 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 BefoulmentSelect from '$lib/components/sidebar/edit/BefoulmentSelect.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import { getElementIcon } from '$lib/utils/images' import { seriesHasWeaponKeys, getSeriesSlug } from '$lib/utils/weaponSeries' @@ -31,7 +33,8 @@ type?: Awakening level: number } | null - axSkills: SimpleAxSkill[] + axSkills: AugmentSkill[] + befoulment?: Befoulment | null } export interface WeaponEditUpdates { @@ -46,10 +49,13 @@ id: string level: number } | null - axModifier1?: number + axModifier1Id?: string axStrength1?: number - axModifier2?: number + axModifier2Id?: string axStrength2?: number + befoulmentModifierId?: string + befoulmentStrength?: number + exorcismLevel?: number } interface Props { @@ -72,14 +78,8 @@ let weaponKey3 = $state(currentValues.weaponKey3Id) let selectedAwakening = $state(currentValues.awakening?.type) let awakeningLevel = $state(currentValues.awakening?.level ?? 1) - let axSkills = $state( - currentValues.axSkills.length > 0 - ? currentValues.axSkills - : [ - { modifier: -1, strength: 0 }, - { modifier: -1, strength: 0 } - ] - ) + let axSkills = $state(currentValues.axSkills ?? []) + let befoulment = $state(currentValues.befoulment ?? null) // Re-initialize when currentValues changes $effect(() => { @@ -91,13 +91,8 @@ 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 } - ] + axSkills = currentValues.axSkills ?? [] + befoulment = currentValues.befoulment ?? null }) // Derived conditions @@ -118,8 +113,10 @@ const hasWeaponKeys = $derived(seriesHasWeaponKeys(series)) const keySlotCount = $derived(seriesSlug ? (WEAPON_KEY_SLOTS[seriesSlug] ?? 2) : 0) - const hasAxSkills = $derived(weaponData?.ax === true) - const axType = $derived(weaponData?.axType ?? 1) + // Augment type from series determines AX skills vs befoulment + const augmentType = $derived(series?.augmentType ?? 'none') + const hasAxSkills = $derived(augmentType === 'ax') + const hasBefoulment = $derived(augmentType === 'befoulment') const hasAwakening = $derived((weaponData?.maxAwakeningLevel ?? 0) > 0) const availableAwakenings = $derived(weaponData?.awakenings ?? []) @@ -196,17 +193,26 @@ } // AX Skills - if (hasAxSkills && axSkills.length >= 2) { - if (axSkills[0] && axSkills[0].modifier >= 0) { - updates.axModifier1 = axSkills[0].modifier + if (hasAxSkills) { + if (axSkills[0]?.modifier?.id) { + updates.axModifier1Id = axSkills[0].modifier.id updates.axStrength1 = axSkills[0].strength } - if (axSkills[1] && axSkills[1].modifier >= 0) { - updates.axModifier2 = axSkills[1].modifier + if (axSkills[1]?.modifier?.id) { + updates.axModifier2Id = axSkills[1].modifier.id updates.axStrength2 = axSkills[1].strength } } + // Befoulment + if (hasBefoulment) { + if (befoulment?.modifier?.id) { + updates.befoulmentModifierId = befoulment.modifier.id + updates.befoulmentStrength = befoulment.strength + updates.exorcismLevel = befoulment.exorcismLevel + } + } + onSave?.(updates) } @@ -279,7 +285,6 @@
{ axSkills = skills @@ -289,6 +294,19 @@ {/if} + {#if hasBefoulment} + +
+ { + befoulment = bef + }} + /> +
+
+ {/if} + {#if hasAwakening && availableAwakenings.length > 0}
diff --git a/src/lib/components/sidebar/EditWeaponSidebar.svelte b/src/lib/components/sidebar/EditWeaponSidebar.svelte index 437afaff..08b9b351 100644 --- a/src/lib/components/sidebar/EditWeaponSidebar.svelte +++ b/src/lib/components/sidebar/EditWeaponSidebar.svelte @@ -1,13 +1,15 @@ @@ -296,7 +311,6 @@
{ axSkills = skills @@ -306,6 +320,19 @@ {/if} + {#if hasBefoulment} + +
+ { + befoulment = bef + }} + /> +
+
+ {/if} + {#if hasAwakening && availableAwakenings.length > 0}
@@ -438,7 +465,8 @@ padding: spacing.$unit; } - .ax-skills-wrapper { + .ax-skills-wrapper, + .befoulment-wrapper { padding: spacing.$unit; } diff --git a/src/lib/components/sidebar/details/TeamView.svelte b/src/lib/components/sidebar/details/TeamView.svelte index d10dfb1c..f23aecd2 100644 --- a/src/lib/components/sidebar/details/TeamView.svelte +++ b/src/lib/components/sidebar/details/TeamView.svelte @@ -105,17 +105,32 @@ {/if} - {#if modificationStatus.hasAxSkills && weapon.ax} + {#if modificationStatus.hasAxSkills && weapon.ax?.length} {#each weapon.ax as axSkill} - + {#if axSkill.modifier?.id} + + {/if} {/each} {/if} + {#if modificationStatus.hasBefoulment && weapon.befoulment?.modifier} + + + + + {/if} + {#if modificationStatus.hasElement && weapon.element} diff --git a/src/lib/components/sidebar/edit/AxSkillSelect.svelte b/src/lib/components/sidebar/edit/AxSkillSelect.svelte index ee2242ff..81c6fe28 100644 --- a/src/lib/components/sidebar/edit/AxSkillSelect.svelte +++ b/src/lib/components/sidebar/edit/AxSkillSelect.svelte @@ -1,215 +1,138 @@ -
- -
-
-
- - {/if} -
- - {#if primaryError} -

{primaryError}

- {/if} +{#if isLoading} +
+
- - - {#if showSecondary} +{:else} +
+
+ {#if selectedPrimary} + + {#if getSuffix(selectedPrimary)} + {getSuffix(selectedPrimary)} {/if} {/if}
- - {#if secondaryError} -

{secondaryError}

- {/if}
- {/if} -
+ + + {#if showSecondary} +
+
+
+ + {#if getSuffix(selectedSecondary)} + {getSuffix(selectedSecondary)} + {/if} + {/if} +
+
+ {/if} +
+{/if} diff --git a/src/lib/components/sidebar/edit/BefoulmentSelect.svelte b/src/lib/components/sidebar/edit/BefoulmentSelect.svelte new file mode 100644 index 00000000..0fc3a02f --- /dev/null +++ b/src/lib/components/sidebar/edit/BefoulmentSelect.svelte @@ -0,0 +1,235 @@ + + + + +{#if isLoading} +
+
+
+{:else} +
+ +
+ + +
+ + +
+ +