Add weapon stat modifier UI for AX skills and befoulments (#448)
## Summary - Add weapon stat modifier types and API layer - Rewrite AX skill components to fetch modifiers from API instead of local data - Add befoulment select component for weapon editing - Update weapon edit views to use new modifier system - Remove old hardcoded ax skill definitions This is the frontend counterpart to the API weapon stat modifiers feature. AX skills and befoulments are now fetched from the API instead of being hardcoded in the frontend. ## Test plan - [ ] Edit a weapon with AX skills, verify dropdown shows correct options - [ ] Add/remove AX skills on a weapon, verify saves correctly - [ ] Add befoulment to a weapon, verify UI and save work - [ ] Verify existing weapons with AX skills display correctly
This commit is contained in:
parent
5d7e8334f9
commit
839365a5a1
24 changed files with 864 additions and 662 deletions
|
|
@ -29,7 +29,7 @@ describe('EntityAdapter', () => {
|
|||
name: { en: 'Dark Opus', ja: 'ダークオーパス' },
|
||||
hasWeaponKeys: true,
|
||||
hasAwakening: true,
|
||||
hasAxSkills: false,
|
||||
augmentType: 'none',
|
||||
extra: false,
|
||||
elementChangeable: false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ describe('GridAdapter', () => {
|
|||
name: { en: 'Dark Opus', ja: 'ダークオーパス' },
|
||||
hasWeaponKeys: true,
|
||||
hasAwakening: true,
|
||||
hasAxSkills: false,
|
||||
augmentType: 'none',
|
||||
extra: false,
|
||||
elementChangeable: false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<WeaponStatModifier[]> {
|
||||
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<WeaponStatModifier[]>(url, {
|
||||
method: 'GET',
|
||||
cacheTTL: 3600000 // Cache for 1 hour - reference data rarely changes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets AX skills only
|
||||
*/
|
||||
async getAxSkills(): Promise<WeaponStatModifier[]> {
|
||||
return this.getWeaponStatModifiers('ax')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets befoulments only
|
||||
*/
|
||||
async getBefoulments(): Promise<WeaponStatModifier[]> {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
|
||||
<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 ax.modifier?.id}
|
||||
<DetailRow label="Skill {i + 1}" value={`${ax.modifier.nameEn} +${ax.strength}${ax.modifier.suffix ?? ''}`} />
|
||||
{/if}
|
||||
{/each}
|
||||
</DetailsSection>
|
||||
|
|
|
|||
|
|
@ -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<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 }
|
||||
]
|
||||
)
|
||||
let axSkills = $state<AugmentSkill[]>(currentValues.axSkills ?? [])
|
||||
let befoulment = $state<Befoulment | null>(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)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -279,7 +285,6 @@
|
|||
<DetailsSection title="AX Skills">
|
||||
<div class="section-content">
|
||||
<AxSkillSelect
|
||||
{axType}
|
||||
currentSkills={axSkills}
|
||||
onChange={(skills) => {
|
||||
axSkills = skills
|
||||
|
|
@ -289,6 +294,19 @@
|
|||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasBefoulment}
|
||||
<DetailsSection title="Befoulment">
|
||||
<div class="section-content">
|
||||
<BefoulmentSelect
|
||||
currentBefoulment={befoulment}
|
||||
onChange={(bef) => {
|
||||
befoulment = bef
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasAwakening && availableAwakenings.length > 0}
|
||||
<DetailsSection title="Awakening">
|
||||
<div class="section-content">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type { GridWeapon } from '$lib/types/api/party'
|
||||
import type { WeaponKey } from '$lib/api/adapters/entity.adapter'
|
||||
import type { Awakening, SimpleAxSkill } from '$lib/types/api/entities'
|
||||
import type { Awakening } from '$lib/types/api/entities'
|
||||
import type { AugmentSkill, Befoulment } from '$lib/types/api/weaponStatModifier'
|
||||
import DetailsSection from './details/DetailsSection.svelte'
|
||||
import ItemHeader from './details/ItemHeader.svelte'
|
||||
import Select from '$lib/components/ui/Select.svelte'
|
||||
import WeaponKeySelect from './edit/WeaponKeySelect.svelte'
|
||||
import AwakeningSelect from './edit/AwakeningSelect.svelte'
|
||||
import AxSkillSelect from './edit/AxSkillSelect.svelte'
|
||||
import BefoulmentSelect from './edit/BefoulmentSelect.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import { getElementIcon } from '$lib/utils/images'
|
||||
|
|
@ -52,12 +54,10 @@
|
|||
let awakeningLevel = $state(weapon.awakening?.level ?? 1)
|
||||
|
||||
// AX skill state - initialize from existing AX skills
|
||||
let axSkills = $state<SimpleAxSkill[]>(
|
||||
weapon.ax ?? [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
)
|
||||
let axSkills = $state<AugmentSkill[]>(weapon.ax ?? [])
|
||||
|
||||
// Befoulment state - initialize from existing befoulment
|
||||
let befoulment = $state<Befoulment | null>(weapon.befoulment ?? null)
|
||||
|
||||
// Weapon data shortcuts
|
||||
const weaponData = $derived(weapon.weapon)
|
||||
|
|
@ -81,8 +81,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 ?? [])
|
||||
|
||||
|
|
@ -125,10 +127,13 @@
|
|||
weaponKey3Id?: string | null
|
||||
awakeningId?: string | null
|
||||
awakeningLevel?: number
|
||||
axModifier1?: number | null
|
||||
axModifier1Id?: string | null
|
||||
axStrength1?: number | null
|
||||
axModifier2?: number | null
|
||||
axModifier2Id?: string | null
|
||||
axStrength2?: number | null
|
||||
befoulmentModifierId?: string | null
|
||||
befoulmentStrength?: number | null
|
||||
exorcismLevel?: number | null
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
|
|
@ -169,32 +174,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
// AX skills - send modifier/strength pairs
|
||||
// AX skills - send modifier IDs and strength values
|
||||
if (hasAxSkills) {
|
||||
const originalAx = weapon.ax ?? [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
const originalAx = weapon.ax ?? []
|
||||
|
||||
const ax1 = axSkills[0]
|
||||
const ax2 = axSkills[1]
|
||||
const origAx1 = originalAx[0]
|
||||
const origAx2 = originalAx[1]
|
||||
|
||||
if (ax1?.modifier !== origAx1?.modifier) {
|
||||
updates.axModifier1 = ax1?.modifier ?? null
|
||||
if (ax1?.modifier?.id !== origAx1?.modifier?.id) {
|
||||
updates.axModifier1Id = ax1?.modifier?.id ?? null
|
||||
}
|
||||
if (ax1?.strength !== origAx1?.strength) {
|
||||
updates.axStrength1 = ax1?.strength ?? null
|
||||
}
|
||||
if (ax2?.modifier !== origAx2?.modifier) {
|
||||
updates.axModifier2 = ax2?.modifier ?? null
|
||||
if (ax2?.modifier?.id !== origAx2?.modifier?.id) {
|
||||
updates.axModifier2Id = ax2?.modifier?.id ?? null
|
||||
}
|
||||
if (ax2?.strength !== origAx2?.strength) {
|
||||
updates.axStrength2 = ax2?.strength ?? null
|
||||
}
|
||||
}
|
||||
|
||||
// Befoulment - send modifier ID, strength, and exorcism level
|
||||
if (hasBefoulment) {
|
||||
const originalBef = weapon.befoulment
|
||||
|
||||
if (befoulment?.modifier?.id !== originalBef?.modifier?.id) {
|
||||
updates.befoulmentModifierId = befoulment?.modifier?.id ?? null
|
||||
}
|
||||
if (befoulment?.strength !== originalBef?.strength) {
|
||||
updates.befoulmentStrength = befoulment?.strength ?? null
|
||||
}
|
||||
if (befoulment?.exorcismLevel !== originalBef?.exorcismLevel) {
|
||||
updates.exorcismLevel = befoulment?.exorcismLevel ?? null
|
||||
}
|
||||
}
|
||||
|
||||
// Only call onSave if there are actual updates
|
||||
if (Object.keys(updates).length > 0) {
|
||||
onSave?.(updates as Partial<GridWeapon>)
|
||||
|
|
@ -212,10 +229,8 @@
|
|||
weaponKey3 = weapon.weaponKeys?.[2]?.id
|
||||
selectedAwakening = weapon.awakening?.type
|
||||
awakeningLevel = weapon.awakening?.level ?? 1
|
||||
axSkills = weapon.ax ?? [
|
||||
{ modifier: -1, strength: 0 },
|
||||
{ modifier: -1, strength: 0 }
|
||||
]
|
||||
axSkills = weapon.ax ?? []
|
||||
befoulment = weapon.befoulment ?? null
|
||||
onCancel?.()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -296,7 +311,6 @@
|
|||
<DetailsSection title="AX Skills">
|
||||
<div class="ax-skills-wrapper">
|
||||
<AxSkillSelect
|
||||
{axType}
|
||||
currentSkills={axSkills}
|
||||
onChange={(skills) => {
|
||||
axSkills = skills
|
||||
|
|
@ -306,6 +320,19 @@
|
|||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasBefoulment}
|
||||
<DetailsSection title="Befoulment">
|
||||
<div class="befoulment-wrapper">
|
||||
<BefoulmentSelect
|
||||
currentBefoulment={befoulment}
|
||||
onChange={(bef) => {
|
||||
befoulment = bef
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if hasAwakening && availableAwakenings.length > 0}
|
||||
<DetailsSection title="Awakening">
|
||||
<div class="awakening-select-wrapper">
|
||||
|
|
@ -438,7 +465,8 @@
|
|||
padding: spacing.$unit;
|
||||
}
|
||||
|
||||
.ax-skills-wrapper {
|
||||
.ax-skills-wrapper,
|
||||
.befoulment-wrapper {
|
||||
padding: spacing.$unit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,17 +105,32 @@
|
|||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasAxSkills && weapon.ax}
|
||||
{#if modificationStatus.hasAxSkills && weapon.ax?.length}
|
||||
<DetailsSection title="AX Skills">
|
||||
{#each weapon.ax as axSkill}
|
||||
<DetailRow
|
||||
label={formatAxSkill(axSkill).split('+')[0]?.trim() ?? ''}
|
||||
value={`+${axSkill.strength}${axSkill.modifier <= 2 ? '' : '%'}`}
|
||||
/>
|
||||
{#if axSkill.modifier?.id}
|
||||
<DetailRow
|
||||
label={axSkill.modifier.nameEn}
|
||||
value={`+${axSkill.strength}${axSkill.modifier.suffix ?? ''}`}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasBefoulment && weapon.befoulment?.modifier}
|
||||
<DetailsSection title="Befoulment">
|
||||
<DetailRow
|
||||
label={weapon.befoulment.modifier.nameEn}
|
||||
value={`${weapon.befoulment.strength}${weapon.befoulment.modifier.suffix ?? ''}`}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Exorcism Level"
|
||||
value={`${weapon.befoulment.exorcismLevel ?? 0}`}
|
||||
/>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasElement && weapon.element}
|
||||
<DetailsSection title="Element Override">
|
||||
<DetailRow label="Weapon Element">
|
||||
|
|
|
|||
|
|
@ -1,215 +1,138 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { SimpleAxSkill } from '$lib/types/api/entities'
|
||||
import {
|
||||
type AxSkill,
|
||||
NO_AX_SKILL,
|
||||
getAxSkillsForType,
|
||||
findPrimarySkill,
|
||||
findSecondarySkill
|
||||
} from '$lib/data/ax'
|
||||
import type { AugmentSkill, WeaponStatModifier } from '$lib/types/api/weaponStatModifier'
|
||||
import { useWeaponStatModifiers } from '$lib/composables/useWeaponStatModifiers.svelte'
|
||||
import Select from '$lib/components/ui/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
/** The weapon's axType (1-3) */
|
||||
axType: number
|
||||
/** Current AX skills on the weapon */
|
||||
currentSkills?: SimpleAxSkill[]
|
||||
currentSkills?: AugmentSkill[]
|
||||
/** Called when skills change */
|
||||
onChange?: (skills: SimpleAxSkill[]) => void
|
||||
onChange?: (skills: AugmentSkill[]) => void
|
||||
/** Language for display */
|
||||
locale?: 'en' | 'ja'
|
||||
}
|
||||
|
||||
let { axType, currentSkills, onChange }: Props = $props()
|
||||
let { currentSkills = [], onChange, locale = 'en' }: Props = $props()
|
||||
|
||||
// Get available primary skills for this axType
|
||||
const primarySkills = $derived(getAxSkillsForType(axType))
|
||||
const { primaryAxSkills, secondaryAxSkills, findAxSkill, isLoading } = useWeaponStatModifiers()
|
||||
|
||||
// State for primary skill
|
||||
let primaryModifier = $state(currentSkills?.[0]?.modifier ?? -1)
|
||||
let primaryValue = $state(currentSkills?.[0]?.strength ?? 0)
|
||||
let primaryError = $state('')
|
||||
let selectedPrimaryId = $state<string>(currentSkills[0]?.modifier?.id ?? '')
|
||||
let primaryStrength = $state<number>(currentSkills[0]?.strength ?? 0)
|
||||
|
||||
// State for secondary skill
|
||||
let secondaryModifier = $state(currentSkills?.[1]?.modifier ?? -1)
|
||||
let secondaryValue = $state(currentSkills?.[1]?.strength ?? 0)
|
||||
let secondaryError = $state('')
|
||||
let selectedSecondaryId = $state<string>(currentSkills[1]?.modifier?.id ?? '')
|
||||
let secondaryStrength = $state<number>(currentSkills[1]?.strength ?? 0)
|
||||
|
||||
// Get the selected primary skill
|
||||
const selectedPrimarySkill = $derived(findPrimarySkill(axType, primaryModifier))
|
||||
// Get selected modifiers
|
||||
const selectedPrimary = $derived(selectedPrimaryId ? findAxSkill(selectedPrimaryId) : undefined)
|
||||
const selectedSecondary = $derived(
|
||||
selectedSecondaryId ? findAxSkill(selectedSecondaryId) : undefined
|
||||
)
|
||||
|
||||
// Whether secondary skill selection should be shown
|
||||
// Hide if no primary skill selected, or if primary skill has no secondaries (like EXP/Rupie)
|
||||
const showSecondary = $derived(
|
||||
primaryModifier >= 0 &&
|
||||
selectedPrimarySkill?.secondary &&
|
||||
selectedPrimarySkill.secondary.length > 0
|
||||
)
|
||||
const showSecondary = $derived(!!selectedPrimary)
|
||||
|
||||
// Build primary skill options
|
||||
const primaryOptions = $derived.by(() => {
|
||||
const items: Array<{ value: number; label: string }> = [
|
||||
{ value: -1, label: NO_AX_SKILL.name.en }
|
||||
]
|
||||
const items: Array<{ value: string; label: string }> = [{ value: '', label: 'No skill' }]
|
||||
|
||||
for (const skill of primarySkills) {
|
||||
for (const skill of primaryAxSkills) {
|
||||
items.push({
|
||||
value: skill.id,
|
||||
label: skill.name.en
|
||||
label: locale === 'ja' ? skill.nameJp : skill.nameEn
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// Build secondary skill options based on selected primary
|
||||
// Build secondary skill options
|
||||
const secondaryOptions = $derived.by(() => {
|
||||
const items: Array<{ value: number; label: string }> = [
|
||||
{ value: -1, label: NO_AX_SKILL.name.en }
|
||||
]
|
||||
const items: Array<{ value: string; label: string }> = [{ value: '', label: 'No skill' }]
|
||||
|
||||
if (selectedPrimarySkill?.secondary) {
|
||||
for (const skill of selectedPrimarySkill.secondary) {
|
||||
items.push({
|
||||
value: skill.id,
|
||||
label: skill.name.en
|
||||
})
|
||||
}
|
||||
for (const skill of secondaryAxSkills) {
|
||||
items.push({
|
||||
value: skill.id,
|
||||
label: locale === 'ja' ? skill.nameJp : skill.nameEn
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// Get range string for input placeholder
|
||||
function getRangeString(skill: AxSkill | undefined): string {
|
||||
if (!skill) return ''
|
||||
return `${skill.minValue}~${skill.maxValue}${skill.suffix || ''}`
|
||||
}
|
||||
|
||||
// Validate a value against a skill's constraints
|
||||
function validateValue(value: number, skill: AxSkill | undefined): string {
|
||||
if (!skill) return ''
|
||||
|
||||
if (isNaN(value) || value <= 0) {
|
||||
return `Please enter a value for ${skill.name.en}`
|
||||
}
|
||||
|
||||
if (value < skill.minValue) {
|
||||
return `${skill.name.en} must be at least ${skill.minValue}${skill.suffix || ''}`
|
||||
}
|
||||
|
||||
if (value > skill.maxValue) {
|
||||
return `${skill.name.en} cannot exceed ${skill.maxValue}${skill.suffix || ''}`
|
||||
}
|
||||
|
||||
if (!skill.fractional && value % 1 !== 0) {
|
||||
return `${skill.name.en} must be a whole number`
|
||||
}
|
||||
|
||||
return ''
|
||||
// Get suffix for display
|
||||
function getSuffix(modifier: WeaponStatModifier | undefined): string {
|
||||
return modifier?.suffix ?? ''
|
||||
}
|
||||
|
||||
// Handle primary skill change
|
||||
function handlePrimaryChange(value: number | undefined) {
|
||||
const newValue = value ?? -1
|
||||
primaryModifier = newValue
|
||||
primaryValue = 0
|
||||
primaryError = ''
|
||||
|
||||
// Reset secondary when primary changes
|
||||
secondaryModifier = -1
|
||||
secondaryValue = 0
|
||||
secondaryError = ''
|
||||
|
||||
function handlePrimaryChange(value: string | undefined) {
|
||||
selectedPrimaryId = value ?? ''
|
||||
if (!value) {
|
||||
primaryStrength = 0
|
||||
// Reset secondary when primary is cleared
|
||||
selectedSecondaryId = ''
|
||||
secondaryStrength = 0
|
||||
}
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Handle primary value change
|
||||
function handlePrimaryValueChange(event: Event) {
|
||||
function handlePrimaryStrengthChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = parseFloat(input.value)
|
||||
primaryValue = value
|
||||
|
||||
primaryError = validateValue(value, selectedPrimarySkill)
|
||||
primaryStrength = parseFloat(input.value) || 0
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Handle secondary skill change
|
||||
function handleSecondaryChange(value: number | undefined) {
|
||||
const newValue = value ?? -1
|
||||
secondaryModifier = newValue
|
||||
secondaryValue = 0
|
||||
secondaryError = ''
|
||||
|
||||
function handleSecondaryChange(value: string | undefined) {
|
||||
selectedSecondaryId = value ?? ''
|
||||
if (!value) {
|
||||
secondaryStrength = 0
|
||||
}
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Handle secondary value change
|
||||
function handleSecondaryValueChange(event: Event) {
|
||||
function handleSecondaryStrengthChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = parseFloat(input.value)
|
||||
secondaryValue = value
|
||||
|
||||
const secondarySkill = findSecondarySkill(selectedPrimarySkill!, secondaryModifier)
|
||||
secondaryError = validateValue(value, secondarySkill)
|
||||
secondaryStrength = parseFloat(input.value) || 0
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Emit change to parent
|
||||
function emitChange() {
|
||||
const skills: SimpleAxSkill[] = [
|
||||
{ modifier: primaryModifier, strength: primaryValue },
|
||||
{ modifier: secondaryModifier, strength: secondaryValue }
|
||||
]
|
||||
const skills: AugmentSkill[] = []
|
||||
|
||||
if (selectedPrimary && primaryStrength > 0) {
|
||||
skills.push({ modifier: selectedPrimary, strength: primaryStrength })
|
||||
}
|
||||
|
||||
if (selectedSecondary && secondaryStrength > 0) {
|
||||
skills.push({ modifier: selectedSecondary, strength: secondaryStrength })
|
||||
}
|
||||
|
||||
onChange?.(skills)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ax-skill-select">
|
||||
<!-- Primary Skill -->
|
||||
<div class="skill-row">
|
||||
<div class="skill-fields">
|
||||
<div class="skill-select">
|
||||
<Select
|
||||
options={primaryOptions}
|
||||
value={primaryModifier}
|
||||
onValueChange={handlePrimaryChange}
|
||||
placeholder="Select skill"
|
||||
size="medium"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if primaryModifier >= 0 && selectedPrimarySkill}
|
||||
<input
|
||||
type="number"
|
||||
class="skill-value"
|
||||
class:error={primaryError !== ''}
|
||||
min={selectedPrimarySkill.minValue}
|
||||
max={selectedPrimarySkill.maxValue}
|
||||
step={selectedPrimarySkill.fractional ? '0.5' : '1'}
|
||||
placeholder={getRangeString(selectedPrimarySkill)}
|
||||
value={primaryValue || ''}
|
||||
oninput={handlePrimaryValueChange}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if primaryError}
|
||||
<p class="error-text">{primaryError}</p>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<div class="ax-skill-select loading">
|
||||
<div class="skeleton"></div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Skill (only shown when primary has secondaries) -->
|
||||
{#if showSecondary}
|
||||
{:else}
|
||||
<div class="ax-skill-select">
|
||||
<!-- Primary Skill -->
|
||||
<div class="skill-row">
|
||||
<div class="skill-fields">
|
||||
<div class="skill-select">
|
||||
<Select
|
||||
options={secondaryOptions}
|
||||
value={secondaryModifier}
|
||||
onValueChange={handleSecondaryChange}
|
||||
options={primaryOptions}
|
||||
value={selectedPrimaryId}
|
||||
onValueChange={handlePrimaryChange}
|
||||
placeholder="Select skill"
|
||||
size="medium"
|
||||
fullWidth
|
||||
|
|
@ -217,30 +140,56 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{#if secondaryModifier >= 0}
|
||||
{@const secondarySkill = findSecondarySkill(selectedPrimarySkill!, secondaryModifier)}
|
||||
{#if secondarySkill}
|
||||
<input
|
||||
type="number"
|
||||
class="skill-value"
|
||||
class:error={secondaryError !== ''}
|
||||
min={secondarySkill.minValue}
|
||||
max={secondarySkill.maxValue}
|
||||
step={secondarySkill.fractional ? '0.5' : '1'}
|
||||
placeholder={getRangeString(secondarySkill)}
|
||||
value={secondaryValue || ''}
|
||||
oninput={handleSecondaryValueChange}
|
||||
/>
|
||||
{#if selectedPrimary}
|
||||
<input
|
||||
type="number"
|
||||
class="skill-value"
|
||||
step="0.5"
|
||||
placeholder="Value"
|
||||
value={primaryStrength || ''}
|
||||
oninput={handlePrimaryStrengthChange}
|
||||
/>
|
||||
{#if getSuffix(selectedPrimary)}
|
||||
<span class="suffix">{getSuffix(selectedPrimary)}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if secondaryError}
|
||||
<p class="error-text">{secondaryError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Secondary Skill -->
|
||||
{#if showSecondary}
|
||||
<div class="skill-row">
|
||||
<div class="skill-fields">
|
||||
<div class="skill-select">
|
||||
<Select
|
||||
options={secondaryOptions}
|
||||
value={selectedSecondaryId}
|
||||
onValueChange={handleSecondaryChange}
|
||||
placeholder="Select skill"
|
||||
size="medium"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if selectedSecondary}
|
||||
<input
|
||||
type="number"
|
||||
class="skill-value"
|
||||
step="0.5"
|
||||
placeholder="Value"
|
||||
value={secondaryStrength || ''}
|
||||
oninput={handleSecondaryStrengthChange}
|
||||
/>
|
||||
{#if getSuffix(selectedSecondary)}
|
||||
<span class="suffix">{getSuffix(selectedSecondary)}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
|
|
@ -252,6 +201,27 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
|
||||
&.loading {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 40px;
|
||||
background: var(--skeleton-bg, colors.$grey-80);
|
||||
border-radius: layout.$item-corner-small;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-row {
|
||||
|
|
@ -272,7 +242,7 @@
|
|||
}
|
||||
|
||||
.skill-value {
|
||||
width: 90px;
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
padding: spacing.$unit spacing.$unit-2x;
|
||||
background: var(--input-bg, colors.$grey-85);
|
||||
|
|
@ -287,10 +257,6 @@
|
|||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: colors.$error;
|
||||
}
|
||||
|
||||
// Remove spin buttons
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button,
|
||||
|
|
@ -300,9 +266,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 0;
|
||||
.suffix {
|
||||
color: var(--text-secondary);
|
||||
font-size: typography.$font-small;
|
||||
color: colors.$error;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
235
src/lib/components/sidebar/edit/BefoulmentSelect.svelte
Normal file
235
src/lib/components/sidebar/edit/BefoulmentSelect.svelte
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { Befoulment, WeaponStatModifier } from '$lib/types/api/weaponStatModifier'
|
||||
import { useWeaponStatModifiers } from '$lib/composables/useWeaponStatModifiers.svelte'
|
||||
import Select from '$lib/components/ui/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
/** Current befoulment on the weapon */
|
||||
currentBefoulment?: Befoulment | null
|
||||
/** Called when befoulment changes */
|
||||
onChange?: (befoulment: Befoulment | null) => void
|
||||
/** Language for display */
|
||||
locale?: 'en' | 'ja'
|
||||
}
|
||||
|
||||
let { currentBefoulment = null, onChange, locale = 'en' }: Props = $props()
|
||||
|
||||
const { befoulments, findBefoulment, isLoading } = useWeaponStatModifiers()
|
||||
|
||||
// State
|
||||
let selectedModifierId = $state<string>(currentBefoulment?.modifier?.id ?? '')
|
||||
let strength = $state<number>(currentBefoulment?.strength ?? 0)
|
||||
let exorcismLevel = $state<number>(currentBefoulment?.exorcismLevel ?? 0)
|
||||
|
||||
// Get selected modifier
|
||||
const selectedModifier = $derived(
|
||||
selectedModifierId ? findBefoulment(selectedModifierId) : undefined
|
||||
)
|
||||
|
||||
// Build befoulment options
|
||||
const befoulmentOptions = $derived.by(() => {
|
||||
const items: Array<{ value: string; label: string }> = [
|
||||
{ value: '', label: 'No befoulment' }
|
||||
]
|
||||
|
||||
for (const bef of befoulments) {
|
||||
items.push({
|
||||
value: bef.id,
|
||||
label: locale === 'ja' ? bef.nameJp : bef.nameEn
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// Exorcism level options (0-5)
|
||||
const exorcismOptions: Array<{ value: number; label: string }> = [
|
||||
{ value: 0, label: 'Level 0 (Full Effect)' },
|
||||
{ value: 1, label: 'Level 1' },
|
||||
{ value: 2, label: 'Level 2' },
|
||||
{ value: 3, label: 'Level 3' },
|
||||
{ value: 4, label: 'Level 4' },
|
||||
{ value: 5, label: 'Level 5 (Fully Exorcised)' }
|
||||
]
|
||||
|
||||
// Get suffix for display
|
||||
function getSuffix(modifier: WeaponStatModifier | undefined): string {
|
||||
return modifier?.suffix ?? ''
|
||||
}
|
||||
|
||||
// Handle befoulment type change
|
||||
function handleModifierChange(value: string | undefined) {
|
||||
selectedModifierId = value ?? ''
|
||||
if (!value) {
|
||||
strength = 0
|
||||
exorcismLevel = 0
|
||||
}
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Handle strength change
|
||||
function handleStrengthChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
strength = parseFloat(input.value) || 0
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Handle exorcism level change
|
||||
function handleExorcismChange(value: number | undefined) {
|
||||
exorcismLevel = value ?? 0
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// Emit change to parent
|
||||
function emitChange() {
|
||||
if (!selectedModifier) {
|
||||
onChange?.(null)
|
||||
return
|
||||
}
|
||||
|
||||
onChange?.({
|
||||
modifier: selectedModifier,
|
||||
strength,
|
||||
exorcismLevel
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="befoulment-select loading">
|
||||
<div class="skeleton"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="befoulment-select">
|
||||
<!-- Befoulment Type -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Befoulment Type</label>
|
||||
<Select
|
||||
options={befoulmentOptions}
|
||||
value={selectedModifierId}
|
||||
onValueChange={handleModifierChange}
|
||||
placeholder="Select befoulment"
|
||||
size="medium"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if selectedModifier}
|
||||
<!-- Strength -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
Strength
|
||||
{#if getSuffix(selectedModifier)}
|
||||
<span class="suffix">({getSuffix(selectedModifier)})</span>
|
||||
{/if}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="strength-input"
|
||||
step="0.5"
|
||||
placeholder="Value"
|
||||
value={strength || ''}
|
||||
oninput={handleStrengthChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Exorcism Level -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Exorcism Level</label>
|
||||
<Select
|
||||
options={exorcismOptions}
|
||||
value={exorcismLevel}
|
||||
onValueChange={handleExorcismChange}
|
||||
size="medium"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
<p class="help-text">Higher exorcism reduces the negative effect</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
|
||||
.befoulment-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
|
||||
&.loading {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 40px;
|
||||
background: var(--skeleton-bg, colors.$grey-80);
|
||||
border-radius: layout.$item-corner-small;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: typography.$font-small;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
|
||||
.suffix {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.strength-input {
|
||||
width: 100%;
|
||||
max-width: 120px;
|
||||
padding: spacing.$unit spacing.$unit-2x;
|
||||
background: var(--input-bg, colors.$grey-85);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: layout.$item-corner-small;
|
||||
color: var(--text-primary);
|
||||
font-size: typography.$font-regular;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
// Remove spin buttons
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin: 0;
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
113
src/lib/composables/useWeaponStatModifiers.svelte.ts
Normal file
113
src/lib/composables/useWeaponStatModifiers.svelte.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Composable for accessing weapon stat modifiers (AX skills and befoulments)
|
||||
*
|
||||
* Provides a reactive interface to fetch and filter weapon stat modifiers
|
||||
* from the API, with derived values for common use cases.
|
||||
*
|
||||
* @module composables/useWeaponStatModifiers
|
||||
*/
|
||||
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import { entityQueries } from '$lib/api/queries/entity.queries'
|
||||
import type { WeaponStatModifier } from '$lib/types/api/weaponStatModifier'
|
||||
|
||||
/**
|
||||
* Primary AX skill slugs - these are the main skills that can have secondaries
|
||||
*/
|
||||
const PRIMARY_AX_SLUGS = ['ax_atk', 'ax_def', 'ax_hp', 'ax_ca_dmg', 'ax_multiattack']
|
||||
|
||||
/**
|
||||
* Composable for accessing weapon stat modifiers
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useWeaponStatModifiers } from '$lib/composables/useWeaponStatModifiers.svelte'
|
||||
*
|
||||
* const { axSkills, befoulments, isLoading } = useWeaponStatModifiers()
|
||||
* </script>
|
||||
*
|
||||
* {#if isLoading}
|
||||
* <p>Loading...</p>
|
||||
* {:else}
|
||||
* <p>Found {axSkills.length} AX skills and {befoulments.length} befoulments</p>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function useWeaponStatModifiers() {
|
||||
// Fetch all weapon stat modifiers
|
||||
const query = createQuery(() => entityQueries.weaponStatModifiers())
|
||||
|
||||
// Filter to AX skills only
|
||||
const axSkills = $derived(
|
||||
(query.data ?? []).filter((m): m is WeaponStatModifier => m.category === 'ax')
|
||||
)
|
||||
|
||||
// Filter to befoulments only
|
||||
const befoulments = $derived(
|
||||
(query.data ?? []).filter((m): m is WeaponStatModifier => m.category === 'befoulment')
|
||||
)
|
||||
|
||||
// Primary AX skills (main skills that can have secondaries)
|
||||
const primaryAxSkills = $derived(axSkills.filter((m) => PRIMARY_AX_SLUGS.includes(m.slug)))
|
||||
|
||||
// Secondary AX skills (all others that aren't primary)
|
||||
const secondaryAxSkills = $derived(axSkills.filter((m) => !PRIMARY_AX_SLUGS.includes(m.slug)))
|
||||
|
||||
/**
|
||||
* Find a modifier by its ID
|
||||
*/
|
||||
function findModifierById(id: string): WeaponStatModifier | undefined {
|
||||
return query.data?.find((m) => m.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a modifier by its slug
|
||||
*/
|
||||
function findModifierBySlug(slug: string): WeaponStatModifier | undefined {
|
||||
return query.data?.find((m) => m.slug === slug)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an AX skill by ID
|
||||
*/
|
||||
function findAxSkill(id: string): WeaponStatModifier | undefined {
|
||||
return axSkills.find((m) => m.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a befoulment by ID
|
||||
*/
|
||||
function findBefoulment(id: string): WeaponStatModifier | undefined {
|
||||
return befoulments.find((m) => m.id === id)
|
||||
}
|
||||
|
||||
return {
|
||||
/** The underlying TanStack Query object */
|
||||
query,
|
||||
/** All modifiers (AX skills + befoulments) */
|
||||
allModifiers: query.data ?? [],
|
||||
/** AX skills only */
|
||||
axSkills,
|
||||
/** Befoulments only */
|
||||
befoulments,
|
||||
/** Primary AX skills (ATK, DEF, HP, C.A. DMG, Multiattack) */
|
||||
primaryAxSkills,
|
||||
/** Secondary AX skills (all others) */
|
||||
secondaryAxSkills,
|
||||
/** Whether the query is loading */
|
||||
isLoading: query.isLoading,
|
||||
/** Whether the query encountered an error */
|
||||
isError: query.isError,
|
||||
/** Error object if query failed */
|
||||
error: query.error,
|
||||
/** Find a modifier by ID */
|
||||
findModifierById,
|
||||
/** Find a modifier by slug */
|
||||
findModifierBySlug,
|
||||
/** Find an AX skill by ID */
|
||||
findAxSkill,
|
||||
/** Find a befoulment by ID */
|
||||
findBefoulment
|
||||
}
|
||||
}
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
/**
|
||||
* AX Skill data for weapon modifications
|
||||
*
|
||||
* Structure:
|
||||
* - Array of 3 AX types (indexed by axType - 1)
|
||||
* - Each type has a list of primary skills
|
||||
* - Primary skills may have secondary skills that can be paired with them
|
||||
*/
|
||||
|
||||
export interface AxSkill {
|
||||
name: {
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
id: number
|
||||
granblueId: string
|
||||
slug: string
|
||||
minValue: number
|
||||
maxValue: number
|
||||
fractional: boolean
|
||||
suffix?: string
|
||||
secondary?: AxSkill[]
|
||||
}
|
||||
|
||||
// "No skill" constant for empty selection
|
||||
export const NO_AX_SKILL: AxSkill = {
|
||||
name: { en: 'No skill', ja: 'スキルなし' },
|
||||
id: -1,
|
||||
granblueId: '',
|
||||
slug: 'no-skill',
|
||||
minValue: 0,
|
||||
maxValue: 0,
|
||||
fractional: false
|
||||
}
|
||||
|
||||
// AX skill data organized by axType (1-indexed in API, 0-indexed here)
|
||||
const ax: AxSkill[][] = [
|
||||
// axType 1 - Standard AX skills
|
||||
[
|
||||
{
|
||||
name: { en: 'ATK', ja: '攻撃' },
|
||||
id: 0,
|
||||
granblueId: '1589',
|
||||
slug: 'atk',
|
||||
minValue: 1,
|
||||
maxValue: 3.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'C.A. DMG', ja: '奥義ダメ' }, id: 3, granblueId: '1591', slug: 'ca-dmg', minValue: 2, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Double Attack Rate', ja: 'DA確率' }, id: 5, granblueId: '1596', slug: 'da', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Triple Attack Rate', ja: 'TA確率' }, id: 6, granblueId: '1597', slug: 'ta', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Skill DMG Cap', ja: 'アビ上限' }, id: 7, granblueId: '1588', slug: 'skill-cap', minValue: 1, maxValue: 2, fractional: true, suffix: '%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'DEF', ja: '防御' },
|
||||
id: 1,
|
||||
granblueId: '1590',
|
||||
slug: 'def',
|
||||
minValue: 1,
|
||||
maxValue: 8,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'HP', ja: 'HP' }, id: 2, granblueId: '1588', slug: 'hp', minValue: 1, maxValue: 3, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Enmity', ja: '背水' }, id: 11, granblueId: '1601', slug: 'enmity', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'HP', ja: 'HP' },
|
||||
id: 2,
|
||||
granblueId: '1588',
|
||||
slug: 'hp',
|
||||
minValue: 1,
|
||||
maxValue: 11,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'DEF', ja: '防御' }, id: 1, granblueId: '1590', slug: 'def', minValue: 1, maxValue: 3, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'C.A. DMG', ja: '奥義ダメ' },
|
||||
id: 3,
|
||||
granblueId: '1591',
|
||||
slug: 'ca-dmg',
|
||||
minValue: 2,
|
||||
maxValue: 8.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'ATK', ja: '攻撃' }, id: 0, granblueId: '1589', slug: 'atk', minValue: 1, maxValue: 1.5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Elemental ATK', ja: '全属性攻撃力' }, id: 13, granblueId: '1594', slug: 'ele-atk', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'C.A. DMG Cap', ja: '奥義上限' }, id: 8, granblueId: '1599', slug: 'ca-cap', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'Multiattack Rate', ja: '連撃率' },
|
||||
id: 4,
|
||||
granblueId: '1592',
|
||||
slug: 'ta',
|
||||
minValue: 1,
|
||||
maxValue: 4,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'C.A. DMG', ja: '奥義ダメ' }, id: 3, granblueId: '1591', slug: 'ca-dmg', minValue: 2, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Elemental ATK', ja: '全属性攻撃力' }, id: 13, granblueId: '1594', slug: 'ele-atk', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Double Attack Rate', ja: 'DA確率' }, id: 5, granblueId: '1596', slug: 'da', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Triple Attack Rate', ja: 'TA確率' }, id: 6, granblueId: '1597', slug: 'ta', minValue: 1, maxValue: 2, fractional: true, suffix: '%' }
|
||||
]
|
||||
}
|
||||
],
|
||||
// axType 2 - Extended AX skills
|
||||
[
|
||||
{
|
||||
name: { en: 'ATK', ja: '攻撃' },
|
||||
id: 0,
|
||||
granblueId: '1589',
|
||||
slug: 'atk',
|
||||
minValue: 1,
|
||||
maxValue: 3.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'C.A. DMG', ja: '奥義ダメ' }, id: 3, granblueId: '1591', slug: 'ca-dmg', minValue: 2, maxValue: 8.5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Multiattack Rate', ja: '連撃確率' }, id: 4, granblueId: '1592', slug: 'ta', minValue: 1.5, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Normal ATK DMG Cap', ja: '通常ダメ上限' }, id: 14, granblueId: '1722', slug: 'na-dmg', minValue: 0.5, maxValue: 1.5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Supplemental Skill DMG', ja: 'アビ与ダメ上昇' }, id: 15, granblueId: '1719', slug: 'skill-supp', minValue: 1, maxValue: 5, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'DEF', ja: '防御' },
|
||||
id: 1,
|
||||
granblueId: '1590',
|
||||
slug: 'def',
|
||||
minValue: 1,
|
||||
maxValue: 8,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'Elemental DMG Reduction', ja: '属性ダメ軽減' }, id: 17, granblueId: '1721', slug: 'ele-def', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Enmity', ja: '背水' }, id: 11, granblueId: '1601', slug: 'enmity', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'HP', ja: 'HP' },
|
||||
id: 2,
|
||||
granblueId: '1588',
|
||||
slug: 'hp',
|
||||
minValue: 1,
|
||||
maxValue: 11,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'Elemental DMG Reduction', ja: '属性ダメ軽減' }, id: 17, granblueId: '1721', slug: 'ele-def', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'C.A. DMG', ja: '奥義ダメ' },
|
||||
id: 3,
|
||||
granblueId: '1591',
|
||||
slug: 'ca-dmg',
|
||||
minValue: 2,
|
||||
maxValue: 8.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'Multiattack Rate', ja: '連撃率' }, id: 4, granblueId: '1592', slug: 'ta', minValue: 1.5, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Supplemental Skill DMG', ja: 'アビ与ダメ上昇' }, id: 15, granblueId: '1719', slug: 'skill-supp', minValue: 1, maxValue: 5, fractional: false },
|
||||
{ name: { en: 'Supplemental C.A. DMG', ja: '奥義与ダメ上昇' }, id: 16, granblueId: '1720', slug: 'ca-supp', minValue: 1, maxValue: 5, fractional: false },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'Multiattack Rate', ja: '連撃率' },
|
||||
id: 4,
|
||||
granblueId: '1592',
|
||||
slug: 'ta',
|
||||
minValue: 1,
|
||||
maxValue: 4,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'Supplemental C.A. DMG', ja: '奥義与ダメ上昇' }, id: 16, granblueId: '1720', slug: 'ca-supp', minValue: 1, maxValue: 5, fractional: false },
|
||||
{ name: { en: 'Normal ATK DMG Cap', ja: '通常ダメ上限' }, id: 14, granblueId: '1722', slug: 'na-cap', minValue: 0.5, maxValue: 1.5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false },
|
||||
{ name: { en: 'Enmity', ja: '背水' }, id: 11, granblueId: '1601', slug: 'enmity', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
}
|
||||
],
|
||||
// axType 3 - Standard + EXP/Rupie skills
|
||||
[
|
||||
{
|
||||
name: { en: 'ATK', ja: '攻撃' },
|
||||
id: 0,
|
||||
granblueId: '1589',
|
||||
slug: 'atk',
|
||||
minValue: 1,
|
||||
maxValue: 3.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'C.A. DMG', ja: '奥義ダメ' }, id: 3, granblueId: '1591', slug: 'ca-dmg', minValue: 2, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Double Attack Rate', ja: 'DA確率' }, id: 5, granblueId: '1596', slug: 'da', minValue: 1, maxValue: 2, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Triple Attack Rate', ja: 'TA確率' }, id: 6, granblueId: '1597', slug: 'ta', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Skill DMG Cap', ja: 'アビ上限' }, id: 7, granblueId: '1588', slug: 'skill-cap', minValue: 1, maxValue: 2, fractional: true, suffix: '%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'DEF', ja: '防御' },
|
||||
id: 1,
|
||||
granblueId: '1590',
|
||||
slug: 'def',
|
||||
minValue: 1,
|
||||
maxValue: 8,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'HP', ja: 'HP' }, id: 2, granblueId: '1588', slug: 'hp', minValue: 1, maxValue: 3, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Enmity', ja: '背水' }, id: 11, granblueId: '1601', slug: 'enmity', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'HP', ja: 'HP' },
|
||||
id: 2,
|
||||
granblueId: '1588',
|
||||
slug: 'hp',
|
||||
minValue: 1,
|
||||
maxValue: 11,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'DEF', ja: '防御' }, id: 1, granblueId: '1590', slug: 'def', minValue: 1, maxValue: 3, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Debuff Resistance', ja: '弱体耐性' }, id: 9, granblueId: '1593', slug: 'debuff', minValue: 1, maxValue: 3, fractional: false, suffix: '%' },
|
||||
{ name: { en: 'Healing', ja: '回復性能' }, id: 10, granblueId: '1595', slug: 'healing', minValue: 2, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'C.A. DMG', ja: '奥義ダメ' },
|
||||
id: 3,
|
||||
granblueId: '1591',
|
||||
slug: 'ca-dmg',
|
||||
minValue: 2,
|
||||
maxValue: 8.5,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'ATK', ja: '攻撃' }, id: 0, granblueId: '1589', slug: 'atk', minValue: 1, maxValue: 1.5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Elemental ATK', ja: '全属性攻撃力' }, id: 13, granblueId: '1594', slug: 'ele-atk', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'C.A. DMG Cap', ja: '奥義上限' }, id: 8, granblueId: '1599', slug: 'ca-dmg', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Stamina', ja: '渾身' }, id: 12, granblueId: '1600', slug: 'stamina', minValue: 1, maxValue: 3, fractional: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { en: 'Multiattack Rate', ja: '連撃率' },
|
||||
id: 4,
|
||||
granblueId: '1592',
|
||||
slug: 'ta',
|
||||
minValue: 1,
|
||||
maxValue: 4,
|
||||
suffix: '%',
|
||||
fractional: true,
|
||||
secondary: [
|
||||
{ name: { en: 'C.A. DMG', ja: '奥義ダメ' }, id: 3, granblueId: '1591', slug: 'ca-dmg', minValue: 2, maxValue: 4, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Elemental ATK', ja: '全属性攻撃力' }, id: 13, granblueId: '1594', slug: 'ele-atk', minValue: 1, maxValue: 5, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Double Attack Rate', ja: 'DA確率' }, id: 5, granblueId: '1596', slug: 'da', minValue: 1, maxValue: 2, fractional: true, suffix: '%' },
|
||||
{ name: { en: 'Triple Attack Rate', ja: 'TA確率' }, id: 6, granblueId: '1597', slug: 'ta', minValue: 1, maxValue: 2, fractional: true, suffix: '%' }
|
||||
]
|
||||
},
|
||||
// Skills without secondary options
|
||||
{
|
||||
name: { en: 'EXP Gain', ja: 'EXP UP' },
|
||||
id: 18,
|
||||
granblueId: '1837',
|
||||
slug: 'exp',
|
||||
minValue: 5,
|
||||
maxValue: 10,
|
||||
suffix: '%',
|
||||
fractional: false
|
||||
},
|
||||
{
|
||||
name: { en: 'Rupie Gain', ja: '獲得ルピ' },
|
||||
id: 19,
|
||||
granblueId: '1838',
|
||||
slug: 'rupie',
|
||||
minValue: 10,
|
||||
maxValue: 20,
|
||||
suffix: '%',
|
||||
fractional: false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/**
|
||||
* Get AX skills for a given axType
|
||||
* @param axType - The weapon's axType (1-indexed from API)
|
||||
* @returns Array of primary AX skills for this type
|
||||
*/
|
||||
export function getAxSkillsForType(axType: number): AxSkill[] {
|
||||
const index = axType - 1
|
||||
if (index < 0 || index >= ax.length) {
|
||||
return []
|
||||
}
|
||||
return ax[index] ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a primary skill by its modifier ID
|
||||
*/
|
||||
export function findPrimarySkill(axType: number, modifierId: number): AxSkill | undefined {
|
||||
const skills = getAxSkillsForType(axType)
|
||||
return skills.find((s) => s.id === modifierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a secondary skill by its modifier ID within a primary skill
|
||||
*/
|
||||
export function findSecondarySkill(primarySkill: AxSkill, modifierId: number): AxSkill | undefined {
|
||||
return primarySkill.secondary?.find((s) => s.id === modifierId)
|
||||
}
|
||||
|
||||
export default ax
|
||||
19
src/lib/types/GridWeapon.d.ts
vendored
19
src/lib/types/GridWeapon.d.ts
vendored
|
|
@ -1,19 +0,0 @@
|
|||
export interface GridWeapon {
|
||||
id: string
|
||||
mainhand: boolean
|
||||
position: number
|
||||
object: Weapon
|
||||
uncap_level: number
|
||||
transcendence_step: number
|
||||
element: number
|
||||
weapon_keys?: Array<WeaponKey>
|
||||
ax?: Array<SimpleAxSkill>
|
||||
awakening?: {
|
||||
type: Awakening
|
||||
level: number
|
||||
}
|
||||
/** Reference to the source collection weapon if linked */
|
||||
collectionWeaponId?: string
|
||||
/** Whether the grid item is out of sync with its collection source */
|
||||
outOfSync?: boolean
|
||||
}
|
||||
4
src/lib/types/SimpleAxSkill.d.ts
vendored
4
src/lib/types/SimpleAxSkill.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
|||
export interface SimpleAxSkill {
|
||||
modifier: number
|
||||
strength: number
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// These define user-owned items with customizations
|
||||
|
||||
import type { Character, Weapon, Summon, JobAccessory, Awakening } from './entities'
|
||||
import type { AugmentSkill, Befoulment } from './weaponStatModifier'
|
||||
|
||||
/**
|
||||
* Extended mastery modifier (used for rings and earrings)
|
||||
|
|
@ -43,7 +44,10 @@ export interface CollectionWeapon {
|
|||
uncapLevel: number
|
||||
transcendenceStep: number
|
||||
element?: number // For element-changeable weapons
|
||||
ax?: Array<{ modifier: number; strength: number }>
|
||||
/** AX skills with full modifier objects */
|
||||
ax?: AugmentSkill[]
|
||||
/** Befoulment for Odiant weapons */
|
||||
befoulment?: Befoulment
|
||||
awakening: {
|
||||
type: Awakening
|
||||
level: number
|
||||
|
|
@ -122,10 +126,15 @@ export interface CollectionWeaponInput {
|
|||
weaponKey4Id?: string
|
||||
awakeningId?: string
|
||||
awakeningLevel?: number
|
||||
axModifier1?: number
|
||||
// AX skills (uses FK IDs for API payload)
|
||||
axModifier1Id?: string
|
||||
axStrength1?: number
|
||||
axModifier2?: number
|
||||
axModifier2Id?: string
|
||||
axStrength2?: number
|
||||
// Befoulment (for Odiant weapons)
|
||||
befoulmentModifierId?: string
|
||||
befoulmentStrength?: number
|
||||
exorcismLevel?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -259,12 +259,6 @@ export interface WeaponKey {
|
|||
order: number
|
||||
}
|
||||
|
||||
// SimpleAxSkill for weapon AX skills
|
||||
export interface SimpleAxSkill {
|
||||
modifier: number
|
||||
strength: number
|
||||
}
|
||||
|
||||
// Guidebook entity
|
||||
export interface Guidebook {
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import type {
|
|||
Guidebook,
|
||||
User,
|
||||
Awakening,
|
||||
WeaponKey,
|
||||
SimpleAxSkill
|
||||
WeaponKey
|
||||
} from './entities'
|
||||
import type { GridArtifact, CollectionArtifact } from './artifact'
|
||||
import type { AugmentSkill, Befoulment } from './weaponStatModifier'
|
||||
|
||||
// Grid item types - these are the junction tables between Party and entities
|
||||
|
||||
|
|
@ -29,7 +29,10 @@ export interface GridWeapon {
|
|||
element?: number
|
||||
weapon: Weapon // Named properly, not "object"
|
||||
weaponKeys?: WeaponKey[]
|
||||
ax?: SimpleAxSkill[]
|
||||
/** AX skills with full modifier objects */
|
||||
ax?: AugmentSkill[]
|
||||
/** Befoulment for Odiant weapons */
|
||||
befoulment?: Befoulment
|
||||
awakening?: {
|
||||
type?: Awakening
|
||||
level?: number
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
* @module types/api/weaponSeries
|
||||
*/
|
||||
|
||||
import type { AugmentType } from './weaponStatModifier'
|
||||
|
||||
/**
|
||||
* Embedded series reference on weapons.
|
||||
* This is the structure returned in weapon.series field.
|
||||
|
|
@ -18,7 +20,8 @@ export interface WeaponSeriesRef {
|
|||
name: { en: string; ja: string }
|
||||
hasWeaponKeys: boolean
|
||||
hasAwakening: boolean
|
||||
hasAxSkills: boolean
|
||||
/** Type of augment this series supports: "ax", "befoulment", or "none" */
|
||||
augmentType: AugmentType
|
||||
extra: boolean
|
||||
elementChangeable: boolean
|
||||
}
|
||||
|
|
@ -37,7 +40,8 @@ export interface WeaponSeries {
|
|||
elementChangeable: boolean
|
||||
hasWeaponKeys: boolean
|
||||
hasAwakening: boolean
|
||||
hasAxSkills: boolean
|
||||
/** Type of augment this series supports: "ax", "befoulment", or "none" */
|
||||
augmentType: AugmentType
|
||||
// Only included in :full view (show endpoint)
|
||||
weaponCount?: number
|
||||
}
|
||||
|
|
@ -54,7 +58,7 @@ export interface WeaponSeriesInput {
|
|||
element_changeable: boolean
|
||||
has_weapon_keys: boolean
|
||||
has_awakening: boolean
|
||||
has_ax_skills: boolean
|
||||
augment_type: AugmentType
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,7 +73,7 @@ export interface CreateWeaponSeriesPayload {
|
|||
element_changeable?: boolean
|
||||
has_weapon_keys?: boolean
|
||||
has_awakening?: boolean
|
||||
has_ax_skills?: boolean
|
||||
augment_type?: AugmentType
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,7 +88,7 @@ export interface UpdateWeaponSeriesPayload {
|
|||
element_changeable?: boolean
|
||||
has_weapon_keys?: boolean
|
||||
has_awakening?: boolean
|
||||
has_ax_skills?: boolean
|
||||
augment_type?: AugmentType
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
61
src/lib/types/api/weaponStatModifier.ts
Normal file
61
src/lib/types/api/weaponStatModifier.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* Weapon Stat Modifier Types
|
||||
*
|
||||
* Type definitions for the unified AX skill and Befoulment system.
|
||||
* These types represent modifiers from the weapon_stat_modifiers API endpoint.
|
||||
*
|
||||
* @module types/api/weaponStatModifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* Augment type enum for weapon series.
|
||||
* Determines whether a weapon series supports AX skills, befoulments, or neither.
|
||||
*/
|
||||
export type AugmentType = 'none' | 'ax' | 'befoulment'
|
||||
|
||||
/**
|
||||
* WeaponStatModifier from the API.
|
||||
* Represents an AX skill or befoulment modifier definition.
|
||||
*/
|
||||
export interface WeaponStatModifier {
|
||||
/** Unique identifier */
|
||||
id: string
|
||||
/** URL-safe identifier (e.g., "ax_atk", "bef_atk_down") */
|
||||
slug: string
|
||||
/** English display name */
|
||||
nameEn: string
|
||||
/** Japanese display name */
|
||||
nameJp: string
|
||||
/** Category: "ax" for AX skills, "befoulment" for negative modifiers */
|
||||
category: 'ax' | 'befoulment'
|
||||
/** The stat this modifier affects (e.g., "atk", "def", "hp") */
|
||||
stat: string
|
||||
/** Polarity: 1 for buffs (positive), -1 for debuffs (negative) */
|
||||
polarity: 1 | -1
|
||||
/** Display suffix for values (e.g., "%") */
|
||||
suffix: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* AX Skill with its modifier and strength value.
|
||||
* Used in GridWeapon and CollectionWeapon for weapons with AX skills.
|
||||
*/
|
||||
export interface AugmentSkill {
|
||||
/** The modifier definition */
|
||||
modifier: WeaponStatModifier
|
||||
/** The strength/value of this skill (e.g., 3.0 for 3% ATK) */
|
||||
strength: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Befoulment with modifier, strength, and exorcism level.
|
||||
* Used for Odiant weapons with negative stat modifiers.
|
||||
*/
|
||||
export interface Befoulment {
|
||||
/** The befoulment modifier definition */
|
||||
modifier: WeaponStatModifier
|
||||
/** The strength/value of this befoulment */
|
||||
strength: number
|
||||
/** Exorcism level (0-5) - higher levels reduce the negative effect */
|
||||
exorcismLevel: number
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export interface ModificationStatus {
|
|||
hasAwakening: boolean
|
||||
hasWeaponKeys: boolean
|
||||
hasAxSkills: boolean
|
||||
hasBefoulment: boolean
|
||||
hasRings: boolean
|
||||
hasEarring: boolean
|
||||
hasPerpetuity: boolean
|
||||
|
|
@ -25,6 +26,7 @@ export function detectModifications(
|
|||
hasAwakening: false,
|
||||
hasWeaponKeys: false,
|
||||
hasAxSkills: false,
|
||||
hasBefoulment: false,
|
||||
hasRings: false,
|
||||
hasEarring: false,
|
||||
hasPerpetuity: false,
|
||||
|
|
@ -60,6 +62,7 @@ export function detectModifications(
|
|||
status.hasAwakening = !!weapon.awakening
|
||||
status.hasWeaponKeys = !!(weapon.weaponKeys && weapon.weaponKeys.length > 0)
|
||||
status.hasAxSkills = !!(weapon.ax && weapon.ax.length > 0)
|
||||
status.hasBefoulment = !!weapon.befoulment?.modifier
|
||||
status.hasTranscendence = !!(weapon.transcendenceStep && weapon.transcendenceStep > 0)
|
||||
status.hasUncapLevel = weapon.uncapLevel !== undefined && weapon.uncapLevel !== null
|
||||
status.hasElement = !!(weapon.element && weapon.weapon?.element === 0)
|
||||
|
|
@ -68,6 +71,7 @@ export function detectModifications(
|
|||
status.hasAwakening ||
|
||||
status.hasWeaponKeys ||
|
||||
status.hasAxSkills ||
|
||||
status.hasBefoulment ||
|
||||
status.hasTranscendence ||
|
||||
status.hasUncapLevel ||
|
||||
status.hasElement
|
||||
|
|
@ -111,13 +115,14 @@ export function canWeaponBeModified(gridWeapon: GridWeapon | undefined): boolean
|
|||
// Weapon keys (series-specific) - use utility function that handles both formats
|
||||
const hasWeaponKeys = seriesHasWeaponKeys(weapon.series)
|
||||
|
||||
// AX skills
|
||||
const hasAxSkills = weapon.ax === true
|
||||
// AX skills or Befoulment - check augmentType from series
|
||||
const augmentType = weapon.series?.augmentType ?? 'none'
|
||||
const hasAugments = augmentType !== 'none'
|
||||
|
||||
// Awakening (maxAwakeningLevel > 0 means it can have awakening)
|
||||
const hasAwakening = (weapon.maxAwakeningLevel ?? 0) > 0
|
||||
|
||||
return canChangeElement || hasWeaponKeys || hasAxSkills || hasAwakening
|
||||
return canChangeElement || hasWeaponKeys || hasAugments || hasAwakening
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,21 +1,7 @@
|
|||
import type { SimpleAxSkill } from '$lib/types/api/entities'
|
||||
import type { AugmentSkill } from '$lib/types/api/weaponStatModifier'
|
||||
import { getRingStat, getEarringStat, getElementalizedEarringStat } from './masteryUtils'
|
||||
import { isWeaponSeriesRef, type WeaponSeriesRef } from '$lib/types/api/weaponSeries'
|
||||
|
||||
const AX_SKILL_NAMES: Record<number, string> = {
|
||||
1: 'Attack',
|
||||
2: 'HP',
|
||||
3: 'Double Attack',
|
||||
4: 'Triple Attack',
|
||||
5: 'C.A. DMG',
|
||||
6: 'C.A. DMG Cap',
|
||||
7: 'Skill DMG',
|
||||
8: 'Skill DMG Cap',
|
||||
9: 'Stamina',
|
||||
10: 'Enmity',
|
||||
11: 'Critical Hit'
|
||||
}
|
||||
|
||||
export function formatRingStat(
|
||||
modifier: number,
|
||||
strength: number,
|
||||
|
|
@ -46,9 +32,9 @@ export function formatEarringStat(
|
|||
return `${statName} +${strength}${stat.suffix}`
|
||||
}
|
||||
|
||||
export function formatAxSkill(ax: SimpleAxSkill): string {
|
||||
const skillName = AX_SKILL_NAMES[ax.modifier] || `Unknown (${ax.modifier})`
|
||||
const suffix = ax.modifier <= 2 ? '' : '%'
|
||||
export function formatAxSkill(ax: AugmentSkill, locale: 'en' | 'ja' = 'en'): string {
|
||||
const skillName = locale === 'ja' ? ax.modifier.nameJp : ax.modifier.nameEn
|
||||
const suffix = ax.modifier.suffix ?? ''
|
||||
return `${skillName} +${ax.strength}${suffix}`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
import type { Awakening, WeaponKey } from '$lib/types/api/entities'
|
||||
import type { SimpleAxSkill } from '$lib/types/SimpleAxSkill'
|
||||
import type { AugmentSkill } from '$lib/types/api/weaponStatModifier'
|
||||
import { isWeaponSeriesRef, type WeaponSeriesRef } from '$lib/types/api/weaponSeries'
|
||||
import { getBasePath } from '$lib/utils/images'
|
||||
|
||||
|
|
@ -120,10 +120,14 @@ export function getAxSkillImage(axSkill?: { slug?: string }): string | null {
|
|||
|
||||
/**
|
||||
* Get all AX skill images for a weapon
|
||||
* Note: This is a placeholder until ax data structure is fully implemented
|
||||
*/
|
||||
export function getAxSkillImages(ax?: SimpleAxSkill[]): Array<{ url: string; alt: string }> {
|
||||
// TODO: Implement when ax data reference is available
|
||||
// This would need to map ax modifiers to actual ax skill data
|
||||
return []
|
||||
export function getAxSkillImages(ax?: AugmentSkill[]): Array<{ url: string; alt: string }> {
|
||||
if (!ax || ax.length === 0) return []
|
||||
|
||||
return ax
|
||||
.filter((skill) => skill.modifier?.slug)
|
||||
.map((skill) => ({
|
||||
url: `${getBasePath()}/ax/${skill.modifier.slug}.png`,
|
||||
alt: skill.modifier.nameEn || skill.modifier.slug || 'AX Skill'
|
||||
}))
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ const mockOmegaSeries = {
|
|||
name: { en: 'Omega', ja: 'マグナ' },
|
||||
hasWeaponKeys: false,
|
||||
hasAwakening: true,
|
||||
hasAxSkills: true,
|
||||
augmentType: 'ax' as const,
|
||||
extra: false,
|
||||
elementChangeable: false
|
||||
};
|
||||
|
|
@ -19,7 +19,7 @@ const mockOpusSeries = {
|
|||
name: { en: 'Opus', ja: 'オプス' },
|
||||
hasWeaponKeys: true,
|
||||
hasAwakening: true,
|
||||
hasAxSkills: false,
|
||||
augmentType: 'none' as const,
|
||||
extra: false,
|
||||
elementChangeable: false
|
||||
};
|
||||
|
|
@ -30,7 +30,7 @@ const mockDraconicSeries = {
|
|||
name: { en: 'Draconic', ja: 'ドラゴニック' },
|
||||
hasWeaponKeys: true,
|
||||
hasAwakening: false,
|
||||
hasAxSkills: false,
|
||||
augmentType: 'none' as const,
|
||||
extra: false,
|
||||
elementChangeable: false
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue