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,170 +1,129 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SimpleAxSkill } from '$lib/types/api/entities'
|
import type { AugmentSkill, WeaponStatModifier } from '$lib/types/api/weaponStatModifier'
|
||||||
import {
|
import { useWeaponStatModifiers } from '$lib/composables/useWeaponStatModifiers.svelte'
|
||||||
type AxSkill,
|
|
||||||
NO_AX_SKILL,
|
|
||||||
getAxSkillsForType,
|
|
||||||
findPrimarySkill,
|
|
||||||
findSecondarySkill
|
|
||||||
} from '$lib/data/ax'
|
|
||||||
import Select from '$lib/components/ui/Select.svelte'
|
import Select from '$lib/components/ui/Select.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** The weapon's axType (1-3) */
|
|
||||||
axType: number
|
|
||||||
/** Current AX skills on the weapon */
|
/** Current AX skills on the weapon */
|
||||||
currentSkills?: SimpleAxSkill[]
|
currentSkills?: AugmentSkill[]
|
||||||
/** Called when skills change */
|
/** 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 { primaryAxSkills, secondaryAxSkills, findAxSkill, isLoading } = useWeaponStatModifiers()
|
||||||
const primarySkills = $derived(getAxSkillsForType(axType))
|
|
||||||
|
|
||||||
// State for primary skill
|
// State for primary skill
|
||||||
let primaryModifier = $state(currentSkills?.[0]?.modifier ?? -1)
|
let selectedPrimaryId = $state<string>(currentSkills[0]?.modifier?.id ?? '')
|
||||||
let primaryValue = $state(currentSkills?.[0]?.strength ?? 0)
|
let primaryStrength = $state<number>(currentSkills[0]?.strength ?? 0)
|
||||||
let primaryError = $state('')
|
|
||||||
|
|
||||||
// State for secondary skill
|
// State for secondary skill
|
||||||
let secondaryModifier = $state(currentSkills?.[1]?.modifier ?? -1)
|
let selectedSecondaryId = $state<string>(currentSkills[1]?.modifier?.id ?? '')
|
||||||
let secondaryValue = $state(currentSkills?.[1]?.strength ?? 0)
|
let secondaryStrength = $state<number>(currentSkills[1]?.strength ?? 0)
|
||||||
let secondaryError = $state('')
|
|
||||||
|
|
||||||
// Get the selected primary skill
|
// Get selected modifiers
|
||||||
const selectedPrimarySkill = $derived(findPrimarySkill(axType, primaryModifier))
|
const selectedPrimary = $derived(selectedPrimaryId ? findAxSkill(selectedPrimaryId) : undefined)
|
||||||
|
const selectedSecondary = $derived(
|
||||||
|
selectedSecondaryId ? findAxSkill(selectedSecondaryId) : undefined
|
||||||
|
)
|
||||||
|
|
||||||
// Whether secondary skill selection should be shown
|
// 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(!!selectedPrimary)
|
||||||
const showSecondary = $derived(
|
|
||||||
primaryModifier >= 0 &&
|
|
||||||
selectedPrimarySkill?.secondary &&
|
|
||||||
selectedPrimarySkill.secondary.length > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Build primary skill options
|
// Build primary skill options
|
||||||
const primaryOptions = $derived.by(() => {
|
const primaryOptions = $derived.by(() => {
|
||||||
const items: Array<{ value: number; label: string }> = [
|
const items: Array<{ value: string; label: string }> = [{ value: '', label: 'No skill' }]
|
||||||
{ value: -1, label: NO_AX_SKILL.name.en }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const skill of primarySkills) {
|
for (const skill of primaryAxSkills) {
|
||||||
items.push({
|
items.push({
|
||||||
value: skill.id,
|
value: skill.id,
|
||||||
label: skill.name.en
|
label: locale === 'ja' ? skill.nameJp : skill.nameEn
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build secondary skill options based on selected primary
|
// Build secondary skill options
|
||||||
const secondaryOptions = $derived.by(() => {
|
const secondaryOptions = $derived.by(() => {
|
||||||
const items: Array<{ value: number; label: string }> = [
|
const items: Array<{ value: string; label: string }> = [{ value: '', label: 'No skill' }]
|
||||||
{ value: -1, label: NO_AX_SKILL.name.en }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (selectedPrimarySkill?.secondary) {
|
for (const skill of secondaryAxSkills) {
|
||||||
for (const skill of selectedPrimarySkill.secondary) {
|
|
||||||
items.push({
|
items.push({
|
||||||
value: skill.id,
|
value: skill.id,
|
||||||
label: skill.name.en
|
label: locale === 'ja' ? skill.nameJp : skill.nameEn
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get range string for input placeholder
|
// Get suffix for display
|
||||||
function getRangeString(skill: AxSkill | undefined): string {
|
function getSuffix(modifier: WeaponStatModifier | undefined): string {
|
||||||
if (!skill) return ''
|
return modifier?.suffix ?? ''
|
||||||
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 ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle primary skill change
|
// Handle primary skill change
|
||||||
function handlePrimaryChange(value: number | undefined) {
|
function handlePrimaryChange(value: string | undefined) {
|
||||||
const newValue = value ?? -1
|
selectedPrimaryId = value ?? ''
|
||||||
primaryModifier = newValue
|
if (!value) {
|
||||||
primaryValue = 0
|
primaryStrength = 0
|
||||||
primaryError = ''
|
// Reset secondary when primary is cleared
|
||||||
|
selectedSecondaryId = ''
|
||||||
// Reset secondary when primary changes
|
secondaryStrength = 0
|
||||||
secondaryModifier = -1
|
}
|
||||||
secondaryValue = 0
|
|
||||||
secondaryError = ''
|
|
||||||
|
|
||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle primary value change
|
// Handle primary value change
|
||||||
function handlePrimaryValueChange(event: Event) {
|
function handlePrimaryStrengthChange(event: Event) {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
const value = parseFloat(input.value)
|
primaryStrength = parseFloat(input.value) || 0
|
||||||
primaryValue = value
|
|
||||||
|
|
||||||
primaryError = validateValue(value, selectedPrimarySkill)
|
|
||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle secondary skill change
|
// Handle secondary skill change
|
||||||
function handleSecondaryChange(value: number | undefined) {
|
function handleSecondaryChange(value: string | undefined) {
|
||||||
const newValue = value ?? -1
|
selectedSecondaryId = value ?? ''
|
||||||
secondaryModifier = newValue
|
if (!value) {
|
||||||
secondaryValue = 0
|
secondaryStrength = 0
|
||||||
secondaryError = ''
|
}
|
||||||
|
|
||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle secondary value change
|
// Handle secondary value change
|
||||||
function handleSecondaryValueChange(event: Event) {
|
function handleSecondaryStrengthChange(event: Event) {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
const value = parseFloat(input.value)
|
secondaryStrength = parseFloat(input.value) || 0
|
||||||
secondaryValue = value
|
|
||||||
|
|
||||||
const secondarySkill = findSecondarySkill(selectedPrimarySkill!, secondaryModifier)
|
|
||||||
secondaryError = validateValue(value, secondarySkill)
|
|
||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit change to parent
|
// Emit change to parent
|
||||||
function emitChange() {
|
function emitChange() {
|
||||||
const skills: SimpleAxSkill[] = [
|
const skills: AugmentSkill[] = []
|
||||||
{ modifier: primaryModifier, strength: primaryValue },
|
|
||||||
{ modifier: secondaryModifier, strength: secondaryValue }
|
if (selectedPrimary && primaryStrength > 0) {
|
||||||
]
|
skills.push({ modifier: selectedPrimary, strength: primaryStrength })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSecondary && secondaryStrength > 0) {
|
||||||
|
skills.push({ modifier: selectedSecondary, strength: secondaryStrength })
|
||||||
|
}
|
||||||
|
|
||||||
onChange?.(skills)
|
onChange?.(skills)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="ax-skill-select loading">
|
||||||
|
<div class="skeleton"></div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="ax-skill-select">
|
<div class="ax-skill-select">
|
||||||
<!-- Primary Skill -->
|
<!-- Primary Skill -->
|
||||||
<div class="skill-row">
|
<div class="skill-row">
|
||||||
|
|
@ -172,7 +131,7 @@
|
||||||
<div class="skill-select">
|
<div class="skill-select">
|
||||||
<Select
|
<Select
|
||||||
options={primaryOptions}
|
options={primaryOptions}
|
||||||
value={primaryModifier}
|
value={selectedPrimaryId}
|
||||||
onValueChange={handlePrimaryChange}
|
onValueChange={handlePrimaryChange}
|
||||||
placeholder="Select skill"
|
placeholder="Select skill"
|
||||||
size="medium"
|
size="medium"
|
||||||
|
|
@ -181,34 +140,30 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if primaryModifier >= 0 && selectedPrimarySkill}
|
{#if selectedPrimary}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="skill-value"
|
class="skill-value"
|
||||||
class:error={primaryError !== ''}
|
step="0.5"
|
||||||
min={selectedPrimarySkill.minValue}
|
placeholder="Value"
|
||||||
max={selectedPrimarySkill.maxValue}
|
value={primaryStrength || ''}
|
||||||
step={selectedPrimarySkill.fractional ? '0.5' : '1'}
|
oninput={handlePrimaryStrengthChange}
|
||||||
placeholder={getRangeString(selectedPrimarySkill)}
|
|
||||||
value={primaryValue || ''}
|
|
||||||
oninput={handlePrimaryValueChange}
|
|
||||||
/>
|
/>
|
||||||
|
{#if getSuffix(selectedPrimary)}
|
||||||
|
<span class="suffix">{getSuffix(selectedPrimary)}</span>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if primaryError}
|
|
||||||
<p class="error-text">{primaryError}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Secondary Skill (only shown when primary has secondaries) -->
|
<!-- Secondary Skill -->
|
||||||
{#if showSecondary}
|
{#if showSecondary}
|
||||||
<div class="skill-row">
|
<div class="skill-row">
|
||||||
<div class="skill-fields">
|
<div class="skill-fields">
|
||||||
<div class="skill-select">
|
<div class="skill-select">
|
||||||
<Select
|
<Select
|
||||||
options={secondaryOptions}
|
options={secondaryOptions}
|
||||||
value={secondaryModifier}
|
value={selectedSecondaryId}
|
||||||
onValueChange={handleSecondaryChange}
|
onValueChange={handleSecondaryChange}
|
||||||
placeholder="Select skill"
|
placeholder="Select skill"
|
||||||
size="medium"
|
size="medium"
|
||||||
|
|
@ -217,30 +172,24 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if secondaryModifier >= 0}
|
{#if selectedSecondary}
|
||||||
{@const secondarySkill = findSecondarySkill(selectedPrimarySkill!, secondaryModifier)}
|
|
||||||
{#if secondarySkill}
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="skill-value"
|
class="skill-value"
|
||||||
class:error={secondaryError !== ''}
|
step="0.5"
|
||||||
min={secondarySkill.minValue}
|
placeholder="Value"
|
||||||
max={secondarySkill.maxValue}
|
value={secondaryStrength || ''}
|
||||||
step={secondarySkill.fractional ? '0.5' : '1'}
|
oninput={handleSecondaryStrengthChange}
|
||||||
placeholder={getRangeString(secondarySkill)}
|
|
||||||
value={secondaryValue || ''}
|
|
||||||
oninput={handleSecondaryValueChange}
|
|
||||||
/>
|
/>
|
||||||
|
{#if getSuffix(selectedSecondary)}
|
||||||
|
<span class="suffix">{getSuffix(selectedSecondary)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if secondaryError}
|
|
||||||
<p class="error-text">{secondaryError}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/colors' as colors;
|
@use '$src/themes/colors' as colors;
|
||||||
|
|
@ -252,6 +201,27 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: spacing.$unit-2x;
|
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 {
|
.skill-row {
|
||||||
|
|
@ -272,7 +242,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-value {
|
.skill-value {
|
||||||
width: 90px;
|
width: 80px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: spacing.$unit spacing.$unit-2x;
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
background: var(--input-bg, colors.$grey-85);
|
background: var(--input-bg, colors.$grey-85);
|
||||||
|
|
@ -287,10 +257,6 @@
|
||||||
border-color: var(--accent-primary);
|
border-color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
|
||||||
border-color: colors.$error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove spin buttons
|
// Remove spin buttons
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
&::-webkit-outer-spin-button,
|
&::-webkit-outer-spin-button,
|
||||||
|
|
@ -300,9 +266,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.suffix {
|
||||||
margin: 0;
|
color: var(--text-secondary);
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
color: colors.$error;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</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