rewrite ax skill components to use API
- add useWeaponStatModifiers composable - rewrite AxSkillSelect to fetch from API - add BefoulmentSelect for odiant weapons
This commit is contained in:
parent
c612aeab74
commit
1d8a22c725
3 changed files with 486 additions and 172 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue