Make parties more DRY (#444)
We extracted utility functions from the Party.svelte component in order to make things more DRY.
This commit is contained in:
parent
f9bb43f214
commit
149f30c538
6 changed files with 649 additions and 333 deletions
|
|
@ -23,6 +23,11 @@
|
|||
import { Gender } from '$lib/utils/jobUtils'
|
||||
import { openJobSelectionSidebar, openJobSkillSelectionSidebar } from '$lib/features/job/openJobSidebar.svelte'
|
||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
import { extractErrorMessage } from '$lib/utils/errors'
|
||||
import { transformSkillsToArray } from '$lib/utils/jobSkills'
|
||||
import { findNextEmptySlot, SLOT_NOT_FOUND } from '$lib/utils/gridHelpers'
|
||||
import { executeGridOperation, removeGridItem, updateGridItem } from '$lib/utils/gridOperations'
|
||||
import { updateGridItemUncap } from '$lib/utils/gridStateUpdater'
|
||||
|
||||
interface Props {
|
||||
party?: Party
|
||||
|
|
@ -122,40 +127,14 @@
|
|||
throw new Error('Cannot swap items in unsaved party')
|
||||
}
|
||||
|
||||
// Both source and target should have items for swap
|
||||
if (!source.itemId || !target.itemId) {
|
||||
throw new Error('Invalid swap operation - missing items')
|
||||
}
|
||||
|
||||
// Call appropriate grid service method based on type
|
||||
if (source.type === 'weapon') {
|
||||
await gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
} else if (source.type === 'character') {
|
||||
await gridService.moveCharacter(
|
||||
party.id,
|
||||
source.itemId,
|
||||
target.position,
|
||||
editKey || undefined,
|
||||
{
|
||||
shortcode: party.shortcode
|
||||
}
|
||||
)
|
||||
} else if (source.type === 'summon') {
|
||||
await gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unknown item type: ${source.type}`)
|
||||
}
|
||||
|
||||
// Clear cache and refresh party data
|
||||
partyService.clearPartyCache(party.shortcode)
|
||||
const updated = await partyService.getByShortcode(party.shortcode)
|
||||
return updated
|
||||
|
||||
throw new Error(`Unknown item type: ${source.type}`)
|
||||
return executeGridOperation(
|
||||
'swap',
|
||||
source,
|
||||
target,
|
||||
{ partyId: party.id, shortcode: party.shortcode, editKey },
|
||||
gridService,
|
||||
partyService
|
||||
)
|
||||
}
|
||||
|
||||
async function handleMove(source: any, target: any): Promise<Party> {
|
||||
|
|
@ -163,36 +142,14 @@
|
|||
throw new Error('Cannot move items in unsaved party')
|
||||
}
|
||||
|
||||
// Source should have an item, target should be empty
|
||||
if (!source.itemId || target.itemId) {
|
||||
throw new Error('Invalid move operation')
|
||||
}
|
||||
|
||||
// Call appropriate grid service method based on type
|
||||
if (source.type === 'character') {
|
||||
await gridService.moveCharacter(
|
||||
party.id,
|
||||
source.itemId,
|
||||
target.position,
|
||||
editKey || undefined,
|
||||
{ shortcode: party.shortcode }
|
||||
)
|
||||
} else if (source.type === 'weapon') {
|
||||
await gridService.moveWeapon(party.id, source.itemId, target.position, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
} else if (source.type === 'summon') {
|
||||
await gridService.moveSummon(party.id, source.itemId, target.position, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unknown item type: ${source.type}`)
|
||||
}
|
||||
|
||||
// Clear cache and refresh party data
|
||||
partyService.clearPartyCache(party.shortcode)
|
||||
const updated = await partyService.getByShortcode(party.shortcode)
|
||||
return updated
|
||||
return executeGridOperation(
|
||||
'move',
|
||||
source,
|
||||
target,
|
||||
{ partyId: party.id, shortcode: party.shortcode, editKey },
|
||||
gridService,
|
||||
partyService
|
||||
)
|
||||
}
|
||||
|
||||
// Localized name helper: accepts either an object with { name: { en, ja } }
|
||||
|
|
@ -405,12 +362,7 @@
|
|||
console.log('[Party] New skill:', skill)
|
||||
|
||||
// Convert skills object to array format expected by API
|
||||
const skillsArray = Object.entries(updatedSkills)
|
||||
.filter(([_, skill]) => skill !== null && skill !== undefined)
|
||||
.map(([slotKey, skill]) => ({
|
||||
id: skill!.id,
|
||||
slot: parseInt(slotKey)
|
||||
}))
|
||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||
|
||||
console.log('[Party] Skills array to send:', skillsArray)
|
||||
|
||||
|
|
@ -420,33 +372,7 @@
|
|||
)
|
||||
party = updated
|
||||
} catch (e: any) {
|
||||
// Extract detailed error message from nested structure
|
||||
let errorDetails = e?.details
|
||||
|
||||
// Navigate through nested details structure
|
||||
while (errorDetails?.details) {
|
||||
errorDetails = errorDetails.details
|
||||
}
|
||||
|
||||
if (errorDetails?.errors) {
|
||||
if (errorDetails.errors.message) {
|
||||
// Simple message format
|
||||
error = errorDetails.errors.message
|
||||
} else {
|
||||
// Field-based errors
|
||||
const errorMessages = Object.entries(errorDetails.errors)
|
||||
.map(([field, messages]) => {
|
||||
if (Array.isArray(messages)) {
|
||||
return messages.join(', ')
|
||||
}
|
||||
return String(messages)
|
||||
})
|
||||
.join('; ')
|
||||
error = errorMessages || e.message || 'Failed to update skill'
|
||||
}
|
||||
} else {
|
||||
error = e?.message || 'Failed to update skill'
|
||||
}
|
||||
error = extractErrorMessage(e, 'Failed to update skill')
|
||||
console.error('Failed to update skill:', e)
|
||||
} finally {
|
||||
loading = false
|
||||
|
|
@ -466,12 +392,7 @@
|
|||
console.log('[Party] Updated jobSkills after removal:', updatedSkills)
|
||||
|
||||
// Convert skills object to array format expected by API
|
||||
const skillsArray = Object.entries(updatedSkills)
|
||||
.filter(([_, skill]) => skill !== null && skill !== undefined)
|
||||
.map(([slotKey, skill]) => ({
|
||||
id: skill!.id,
|
||||
slot: parseInt(slotKey)
|
||||
}))
|
||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||
|
||||
console.log('[Party] Skills array to send after removal:', skillsArray)
|
||||
|
||||
|
|
@ -481,33 +402,7 @@
|
|||
)
|
||||
party = updated
|
||||
} catch (e: any) {
|
||||
// Extract detailed error message from nested structure
|
||||
let errorDetails = e?.details
|
||||
|
||||
// Navigate through nested details structure
|
||||
while (errorDetails?.details) {
|
||||
errorDetails = errorDetails.details
|
||||
}
|
||||
|
||||
if (errorDetails?.errors) {
|
||||
if (errorDetails.errors.message) {
|
||||
// Simple message format
|
||||
error = errorDetails.errors.message
|
||||
} else {
|
||||
// Field-based errors
|
||||
const errorMessages = Object.entries(errorDetails.errors)
|
||||
.map(([field, messages]) => {
|
||||
if (Array.isArray(messages)) {
|
||||
return messages.join(', ')
|
||||
}
|
||||
return String(messages)
|
||||
})
|
||||
.join('; ')
|
||||
error = errorMessages || e.message || 'Failed to remove skill'
|
||||
}
|
||||
} else {
|
||||
error = e?.message || 'Failed to remove skill'
|
||||
}
|
||||
error = extractErrorMessage(e, 'Failed to remove skill')
|
||||
console.error('Failed to remove skill:', e)
|
||||
} finally {
|
||||
loading = false
|
||||
|
|
@ -529,12 +424,7 @@
|
|||
delete updatedSkills[String(slot) as keyof typeof updatedSkills]
|
||||
|
||||
// Convert skills object to array format expected by API
|
||||
const skillsArray = Object.entries(updatedSkills)
|
||||
.filter(([_, skill]) => skill !== null && skill !== undefined)
|
||||
.map(([slotKey, skill]) => ({
|
||||
id: skill!.id,
|
||||
slot: parseInt(slotKey)
|
||||
}))
|
||||
const skillsArray = transformSkillsToArray(updatedSkills)
|
||||
|
||||
const updated = await partyAdapter.updateJobSkills(
|
||||
party.shortcode,
|
||||
|
|
@ -590,50 +480,8 @@
|
|||
party = updated
|
||||
|
||||
// Find next empty slot for continuous adding
|
||||
let nextEmptySlot = -999 // sentinel value meaning no empty slot found
|
||||
|
||||
if (activeTab === GridType.Weapon) {
|
||||
// Check mainhand first (position -1)
|
||||
if (!party.weapons.find((w) => w.position === -1 || w.mainhand)) {
|
||||
nextEmptySlot = -1
|
||||
} else {
|
||||
// Check grid slots 0-8
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (!party.weapons.find((w) => w.position === i)) {
|
||||
nextEmptySlot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (activeTab === GridType.Summon) {
|
||||
// Check main summon first (position -1)
|
||||
if (!party.summons.find((s) => s.position === -1 || s.main)) {
|
||||
nextEmptySlot = -1
|
||||
} else {
|
||||
// Check grid slots 0-5
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (!party.summons.find((s) => s.position === i)) {
|
||||
nextEmptySlot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// Check friend summon (position 6)
|
||||
if (nextEmptySlot === -999 && !party.summons.find((s) => s.position === 6 || s.friend)) {
|
||||
nextEmptySlot = 6
|
||||
}
|
||||
}
|
||||
} else if (activeTab === GridType.Character) {
|
||||
// Check character slots 0-4
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!party.characters.find((c) => c.position === i)) {
|
||||
nextEmptySlot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's another empty slot, update selectedSlot to it
|
||||
if (nextEmptySlot !== -999) {
|
||||
const nextEmptySlot = findNextEmptySlot(party, activeTab)
|
||||
if (nextEmptySlot !== SLOT_NOT_FOUND) {
|
||||
selectedSlot = nextEmptySlot
|
||||
}
|
||||
// Note: Sidebar stays open for continuous adding
|
||||
|
|
@ -660,17 +508,15 @@
|
|||
const clientGridService = {
|
||||
async removeWeapon(partyId: string, gridWeaponId: string, _editKey?: string) {
|
||||
try {
|
||||
// Remove returns null, so we need to update local state
|
||||
await gridService.removeWeapon(partyId, gridWeaponId, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
|
||||
// Update local state by removing the weapon
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.weapons) {
|
||||
updatedParty.weapons = updatedParty.weapons.filter((w: any) => w.id !== gridWeaponId)
|
||||
}
|
||||
return updatedParty
|
||||
return await removeGridItem(
|
||||
'weapon',
|
||||
partyId,
|
||||
gridWeaponId,
|
||||
party,
|
||||
party.shortcode,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to remove weapon:', err)
|
||||
throw err
|
||||
|
|
@ -678,17 +524,15 @@
|
|||
},
|
||||
async removeSummon(partyId: string, gridSummonId: string, _editKey?: string) {
|
||||
try {
|
||||
// Remove returns null, so we need to update local state
|
||||
await gridService.removeSummon(partyId, gridSummonId, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
|
||||
// Update local state by removing the summon
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.summons) {
|
||||
updatedParty.summons = updatedParty.summons.filter((s: any) => s.id !== gridSummonId)
|
||||
}
|
||||
return updatedParty
|
||||
return await removeGridItem(
|
||||
'summon',
|
||||
partyId,
|
||||
gridSummonId,
|
||||
party,
|
||||
party.shortcode,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to remove summon:', err)
|
||||
throw err
|
||||
|
|
@ -696,19 +540,15 @@
|
|||
},
|
||||
async removeCharacter(partyId: string, gridCharacterId: string, _editKey?: string) {
|
||||
try {
|
||||
// Remove returns null, so we need to update local state
|
||||
await gridService.removeCharacter(partyId, gridCharacterId, editKey || undefined, {
|
||||
shortcode: party.shortcode
|
||||
})
|
||||
|
||||
// Update local state by removing the character
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.characters) {
|
||||
updatedParty.characters = updatedParty.characters.filter(
|
||||
(c: any) => c.id !== gridCharacterId
|
||||
)
|
||||
}
|
||||
return updatedParty
|
||||
return await removeGridItem(
|
||||
'character',
|
||||
partyId,
|
||||
gridCharacterId,
|
||||
party,
|
||||
party.shortcode,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to remove character:', err)
|
||||
throw err
|
||||
|
|
@ -716,14 +556,7 @@
|
|||
},
|
||||
async updateWeapon(partyId: string, gridWeaponId: string, updates: any, _editKey?: string) {
|
||||
try {
|
||||
// Use the grid service to update weapon
|
||||
const updated = await gridService.updateWeapon(
|
||||
partyId,
|
||||
gridWeaponId,
|
||||
updates,
|
||||
editKey || undefined
|
||||
)
|
||||
return updated
|
||||
return await updateGridItem('weapon', partyId, gridWeaponId, updates, editKey, gridService)
|
||||
} catch (err) {
|
||||
console.error('Failed to update weapon:', err)
|
||||
throw err
|
||||
|
|
@ -731,14 +564,7 @@
|
|||
},
|
||||
async updateSummon(partyId: string, gridSummonId: string, updates: any, _editKey?: string) {
|
||||
try {
|
||||
// Use the grid service to update summon
|
||||
const updated = await gridService.updateSummon(
|
||||
partyId,
|
||||
gridSummonId,
|
||||
updates,
|
||||
editKey || undefined
|
||||
)
|
||||
return updated
|
||||
return await updateGridItem('summon', partyId, gridSummonId, updates, editKey, gridService)
|
||||
} catch (err) {
|
||||
console.error('Failed to update summon:', err)
|
||||
throw err
|
||||
|
|
@ -751,14 +577,7 @@
|
|||
_editKey?: string
|
||||
) {
|
||||
try {
|
||||
// Use the grid service to update character
|
||||
const updated = await gridService.updateCharacter(
|
||||
partyId,
|
||||
gridCharacterId,
|
||||
updates,
|
||||
editKey || undefined
|
||||
)
|
||||
return updated
|
||||
return await updateGridItem('character', partyId, gridCharacterId, updates, editKey, gridService)
|
||||
} catch (err) {
|
||||
console.error('Failed to update character:', err)
|
||||
throw err
|
||||
|
|
@ -771,40 +590,14 @@
|
|||
_editKey?: string
|
||||
) {
|
||||
try {
|
||||
const response = await gridService.updateCharacterUncap(
|
||||
return await updateGridItemUncap(
|
||||
'character',
|
||||
{ gridItemId: gridCharacterId, uncapLevel, transcendenceStep },
|
||||
party.id,
|
||||
gridCharacterId,
|
||||
uncapLevel,
|
||||
transcendenceStep,
|
||||
editKey || undefined
|
||||
party,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
// The API returns {gridCharacter: {...}} with the updated item only (transformed to camelCase)
|
||||
// We need to update just that character in the current party state
|
||||
if (response.gridCharacter || response.grid_character) {
|
||||
const updatedChar = response.gridCharacter || response.grid_character
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.characters) {
|
||||
const charIndex = updatedParty.characters.findIndex(
|
||||
(c: any) => c.id === gridCharacterId
|
||||
)
|
||||
if (charIndex !== -1) {
|
||||
// Preserve the character object reference but update uncap fields
|
||||
const existingChar = updatedParty.characters[charIndex]
|
||||
if (existingChar) {
|
||||
updatedParty.characters[charIndex] = {
|
||||
...existingChar,
|
||||
id: existingChar.id,
|
||||
position: existingChar.position,
|
||||
character: existingChar.character,
|
||||
uncapLevel: updatedChar.uncapLevel ?? updatedChar.uncap_level,
|
||||
transcendenceStep: updatedChar.transcendenceStep ?? updatedChar.transcendence_step
|
||||
}
|
||||
}
|
||||
return updatedParty
|
||||
}
|
||||
}
|
||||
}
|
||||
return party // Return unchanged party if update failed
|
||||
} catch (err) {
|
||||
console.error('Failed to update character uncap:', err)
|
||||
throw err
|
||||
|
|
@ -817,39 +610,14 @@
|
|||
_editKey?: string
|
||||
) {
|
||||
try {
|
||||
const response = await gridService.updateWeaponUncap(
|
||||
return await updateGridItemUncap(
|
||||
'weapon',
|
||||
{ gridItemId: gridWeaponId, uncapLevel, transcendenceStep },
|
||||
party.id,
|
||||
gridWeaponId,
|
||||
uncapLevel,
|
||||
transcendenceStep,
|
||||
editKey || undefined
|
||||
party,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
// The API returns {gridWeapon: {...}} with the updated item only (transformed to camelCase)
|
||||
// We need to update just that weapon in the current party state
|
||||
if (response.gridWeapon || response.grid_weapon) {
|
||||
const updatedWeapon = response.gridWeapon || response.grid_weapon
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.weapons) {
|
||||
const weaponIndex = updatedParty.weapons.findIndex((w: any) => w.id === gridWeaponId)
|
||||
if (weaponIndex !== -1) {
|
||||
// Preserve the weapon object reference but update uncap fields
|
||||
const existingWeapon = updatedParty.weapons[weaponIndex]
|
||||
if (existingWeapon) {
|
||||
updatedParty.weapons[weaponIndex] = {
|
||||
...existingWeapon,
|
||||
id: existingWeapon.id,
|
||||
position: existingWeapon.position,
|
||||
weapon: existingWeapon.weapon,
|
||||
uncapLevel: updatedWeapon.uncapLevel ?? updatedWeapon.uncap_level,
|
||||
transcendenceStep:
|
||||
updatedWeapon.transcendenceStep ?? updatedWeapon.transcendence_step
|
||||
}
|
||||
}
|
||||
return updatedParty
|
||||
}
|
||||
}
|
||||
}
|
||||
return party // Return unchanged party if update failed
|
||||
} catch (err) {
|
||||
console.error('Failed to update weapon uncap:', err)
|
||||
throw err
|
||||
|
|
@ -862,39 +630,14 @@
|
|||
_editKey?: string
|
||||
) {
|
||||
try {
|
||||
const response = await gridService.updateSummonUncap(
|
||||
return await updateGridItemUncap(
|
||||
'summon',
|
||||
{ gridItemId: gridSummonId, uncapLevel, transcendenceStep },
|
||||
party.id,
|
||||
gridSummonId,
|
||||
uncapLevel,
|
||||
transcendenceStep,
|
||||
editKey || undefined
|
||||
party,
|
||||
editKey,
|
||||
gridService
|
||||
)
|
||||
// The API returns {gridSummon: {...}} with the updated item only (transformed to camelCase)
|
||||
// We need to update just that summon in the current party state
|
||||
if (response.gridSummon || response.grid_summon) {
|
||||
const updatedSummon = response.gridSummon || response.grid_summon
|
||||
const updatedParty = { ...party }
|
||||
if (updatedParty.summons) {
|
||||
const summonIndex = updatedParty.summons.findIndex((s: any) => s.id === gridSummonId)
|
||||
if (summonIndex !== -1) {
|
||||
// Preserve the summon object reference but update uncap fields
|
||||
const existingSummon = updatedParty.summons[summonIndex]
|
||||
if (existingSummon) {
|
||||
updatedParty.summons[summonIndex] = {
|
||||
...existingSummon,
|
||||
id: existingSummon.id,
|
||||
position: existingSummon.position,
|
||||
summon: existingSummon.summon,
|
||||
uncapLevel: updatedSummon.uncapLevel ?? updatedSummon.uncap_level,
|
||||
transcendenceStep:
|
||||
updatedSummon.transcendenceStep ?? updatedSummon.transcendence_step
|
||||
}
|
||||
}
|
||||
return updatedParty
|
||||
}
|
||||
}
|
||||
}
|
||||
return party // Return unchanged party if update failed
|
||||
} catch (err) {
|
||||
console.error('Failed to update summon uncap:', err)
|
||||
throw err
|
||||
|
|
|
|||
64
src/lib/utils/errors.ts
Normal file
64
src/lib/utils/errors.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Error message extraction utilities
|
||||
* Handles complex nested error structures from API responses
|
||||
*/
|
||||
|
||||
export interface NestedErrorDetails {
|
||||
details?: NestedErrorDetails
|
||||
errors?: { message?: string; [key: string]: any }
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts user-friendly error message from nested API error structures
|
||||
* Handles the pattern: error.details.details.errors.message
|
||||
*
|
||||
* @param error - The error object to extract from
|
||||
* @param fallbackMessage - Message to return if extraction fails
|
||||
* @returns Extracted error message or fallback
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* await api.updateParty(...)
|
||||
* } catch (e) {
|
||||
* error = extractErrorMessage(e, 'Failed to update party')
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function extractErrorMessage(
|
||||
error: any,
|
||||
fallbackMessage: string = 'An error occurred'
|
||||
): string {
|
||||
if (!error) return fallbackMessage
|
||||
|
||||
// Navigate through nested details structure
|
||||
let errorDetails: NestedErrorDetails | undefined = error?.details
|
||||
while (errorDetails?.details) {
|
||||
errorDetails = errorDetails.details
|
||||
}
|
||||
|
||||
// Try to extract message from various formats
|
||||
if (errorDetails?.errors) {
|
||||
// Simple message format
|
||||
if (errorDetails.errors.message) {
|
||||
return errorDetails.errors.message
|
||||
}
|
||||
|
||||
// Field-based errors - combine all messages
|
||||
const errorMessages = Object.entries(errorDetails.errors)
|
||||
.map(([field, messages]) => {
|
||||
if (Array.isArray(messages)) {
|
||||
return messages.join(', ')
|
||||
}
|
||||
return String(messages)
|
||||
})
|
||||
.filter((msg) => msg && msg !== 'undefined')
|
||||
.join('; ')
|
||||
|
||||
if (errorMessages) return errorMessages
|
||||
}
|
||||
|
||||
// Fallback to error.message
|
||||
return error?.message || fallbackMessage
|
||||
}
|
||||
99
src/lib/utils/gridHelpers.ts
Normal file
99
src/lib/utils/gridHelpers.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Grid slot finding and helper utilities
|
||||
* Handles finding available positions in weapon, summon, and character grids
|
||||
*/
|
||||
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { GridType } from '$lib/types/enums'
|
||||
|
||||
/** Sentinel value indicating no empty slot was found */
|
||||
export const SLOT_NOT_FOUND = -999
|
||||
|
||||
export interface SlotRange {
|
||||
start: number
|
||||
end: number
|
||||
specialSlots?: number[] // e.g., mainhand (-1), friend summon (6)
|
||||
}
|
||||
|
||||
/** Grid slot configuration for each grid type */
|
||||
const GRID_CONFIGS: Record<GridType, SlotRange> = {
|
||||
[GridType.Weapon]: { start: 0, end: 8, specialSlots: [-1] }, // mainhand + 9 grid slots
|
||||
[GridType.Summon]: { start: 0, end: 5, specialSlots: [-1, 6] }, // main + 6 grid + friend
|
||||
[GridType.Character]: { start: 0, end: 4, specialSlots: [] } // 5 slots (0-4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next empty slot in a grid
|
||||
*
|
||||
* @param party - Current party state
|
||||
* @param gridType - Type of grid to search (weapon, summon, or character)
|
||||
* @returns Position number of next empty slot, or SLOT_NOT_FOUND if grid is full
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const nextSlot = findNextEmptySlot(party, GridType.Weapon)
|
||||
* if (nextSlot !== SLOT_NOT_FOUND) {
|
||||
* selectedSlot = nextSlot
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function findNextEmptySlot(party: Party, gridType: GridType): number {
|
||||
const config = GRID_CONFIGS[gridType]
|
||||
const collection = getCollectionForType(party, gridType)
|
||||
|
||||
// Check special slots first (e.g., mainhand, main summon)
|
||||
for (const specialSlot of config.specialSlots || []) {
|
||||
if (!isSlotOccupied(collection, specialSlot, gridType)) {
|
||||
return specialSlot
|
||||
}
|
||||
}
|
||||
|
||||
// Check regular grid slots
|
||||
for (let i = config.start; i <= config.end; i++) {
|
||||
if (!isSlotOccupied(collection, i, gridType)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return SLOT_NOT_FOUND
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate collection array for a grid type
|
||||
*/
|
||||
function getCollectionForType(party: Party, gridType: GridType) {
|
||||
switch (gridType) {
|
||||
case GridType.Weapon:
|
||||
return party.weapons
|
||||
case GridType.Summon:
|
||||
return party.summons
|
||||
case GridType.Character:
|
||||
return party.characters
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific slot position is occupied
|
||||
* Handles special cases for mainhand weapons, main/friend summons
|
||||
*/
|
||||
function isSlotOccupied(collection: any[], position: number, gridType: GridType): boolean {
|
||||
// For weapons, check both position and mainhand flag
|
||||
if (gridType === GridType.Weapon) {
|
||||
return collection.some(
|
||||
(item) => item.position === position || (position === -1 && item.mainhand)
|
||||
)
|
||||
}
|
||||
|
||||
// For summons, check position, main, and friend flags
|
||||
if (gridType === GridType.Summon) {
|
||||
return collection.some(
|
||||
(item) =>
|
||||
item.position === position ||
|
||||
(position === -1 && item.main) ||
|
||||
(position === 6 && item.friend)
|
||||
)
|
||||
}
|
||||
|
||||
// For characters, simple position check
|
||||
return collection.some((item) => item.position === position)
|
||||
}
|
||||
207
src/lib/utils/gridOperations.ts
Normal file
207
src/lib/utils/gridOperations.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* Grid operation utilities
|
||||
* Consolidates duplicated grid CRUD logic
|
||||
*/
|
||||
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import type { GridService } from '$lib/services/grid.service'
|
||||
import type { PartyService } from '$lib/services/party.service'
|
||||
|
||||
export type GridItemType = 'character' | 'weapon' | 'summon'
|
||||
export type GridCollection = 'characters' | 'weapons' | 'summons'
|
||||
|
||||
/**
|
||||
* Maps grid item type to collection key in Party object
|
||||
*
|
||||
* @param type - Grid item type (character, weapon, or summon)
|
||||
* @returns Collection key name
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const key = getCollectionKey('weapon') // Returns: 'weapons'
|
||||
* const items = party[key] // Access party.weapons
|
||||
* ```
|
||||
*/
|
||||
export function getCollectionKey(type: GridItemType): GridCollection {
|
||||
const map: Record<GridItemType, GridCollection> = {
|
||||
character: 'characters',
|
||||
weapon: 'weapons',
|
||||
summon: 'summons'
|
||||
}
|
||||
return map[type]
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps operation and grid type to service method name
|
||||
*
|
||||
* @param operation - CRUD operation type
|
||||
* @param type - Grid item type
|
||||
* @returns Method name on GridService
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const methodName = getGridMethodName('add', 'weapon') // Returns: 'addWeapon'
|
||||
* const methodName = getGridMethodName('remove', 'character') // Returns: 'removeCharacter'
|
||||
* ```
|
||||
*/
|
||||
export function getGridMethodName(
|
||||
operation: 'add' | 'move' | 'remove' | 'update',
|
||||
type: GridItemType
|
||||
): string {
|
||||
const typeCapitalized = type.charAt(0).toUpperCase() + type.slice(1)
|
||||
return `${operation}${typeCapitalized}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute grid move/swap operation
|
||||
* Consolidates handleSwap and handleMove logic
|
||||
*
|
||||
* @param operationType - Type of operation (move or swap)
|
||||
* @param source - Source item information
|
||||
* @param target - Target position information
|
||||
* @param context - Party context (ID, shortcode, edit key)
|
||||
* @param gridService - Grid service instance
|
||||
* @param partyService - Party service instance
|
||||
* @returns Updated party data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await executeGridOperation(
|
||||
* 'swap',
|
||||
* { type: 'weapon', itemId: 'abc123', position: 0 },
|
||||
* { type: 'weapon', position: 1, itemId: 'def456' },
|
||||
* { partyId: party.id, shortcode: party.shortcode, editKey },
|
||||
* gridService,
|
||||
* partyService
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export async function executeGridOperation(
|
||||
operationType: 'move' | 'swap',
|
||||
source: { type: GridItemType; itemId: string; position: number },
|
||||
target: { type: GridItemType; position: number; itemId?: string },
|
||||
context: { partyId: string; shortcode: string; editKey?: string },
|
||||
gridService: GridService,
|
||||
partyService: PartyService
|
||||
): Promise<Party> {
|
||||
// Validation
|
||||
if (operationType === 'swap' && !target.itemId) {
|
||||
throw new Error('Swap operation requires target item')
|
||||
}
|
||||
if (operationType === 'move' && target.itemId) {
|
||||
throw new Error('Move operation requires empty target')
|
||||
}
|
||||
|
||||
// Call appropriate grid service method
|
||||
const methodName = getGridMethodName('move', source.type)
|
||||
const method = (gridService as any)[methodName]
|
||||
|
||||
if (!method) {
|
||||
throw new Error(`Unknown grid method: ${methodName}`)
|
||||
}
|
||||
|
||||
await method.call(
|
||||
gridService,
|
||||
context.partyId,
|
||||
source.itemId,
|
||||
target.position,
|
||||
context.editKey,
|
||||
{ shortcode: context.shortcode }
|
||||
)
|
||||
|
||||
// Clear cache and refresh party
|
||||
partyService.clearPartyCache(context.shortcode)
|
||||
return await partyService.getByShortcode(context.shortcode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic grid item remover
|
||||
* Replaces three similar remove{Type} methods in clientGridService
|
||||
*
|
||||
* @param type - Grid item type to remove
|
||||
* @param partyId - Party UUID
|
||||
* @param gridItemId - Grid item UUID to remove
|
||||
* @param party - Current party state
|
||||
* @param shortcode - Party shortcode for cache clearing
|
||||
* @param editKey - Optional edit key for authorization
|
||||
* @param gridService - Grid service instance
|
||||
* @returns Updated party with item removed
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await removeGridItem(
|
||||
* 'weapon',
|
||||
* party.id,
|
||||
* gridWeaponId,
|
||||
* party,
|
||||
* party.shortcode,
|
||||
* editKey,
|
||||
* gridService
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export async function removeGridItem(
|
||||
type: GridItemType,
|
||||
partyId: string,
|
||||
gridItemId: string,
|
||||
party: Party,
|
||||
shortcode: string,
|
||||
editKey: string | undefined,
|
||||
gridService: GridService
|
||||
): Promise<Party> {
|
||||
// Call appropriate remove method
|
||||
const methodName = getGridMethodName('remove', type)
|
||||
const method = (gridService as any)[methodName]
|
||||
|
||||
await method.call(gridService, partyId, gridItemId, editKey, { shortcode })
|
||||
|
||||
// Update local state by removing item
|
||||
const collection = getCollectionKey(type)
|
||||
const updatedParty = { ...party }
|
||||
|
||||
if (updatedParty[collection]) {
|
||||
updatedParty[collection] = updatedParty[collection].filter(
|
||||
(item: any) => item.id !== gridItemId
|
||||
)
|
||||
}
|
||||
|
||||
return updatedParty
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic grid item updater
|
||||
* Replaces three similar update{Type} methods
|
||||
*
|
||||
* @param type - Grid item type to update
|
||||
* @param partyId - Party UUID
|
||||
* @param gridItemId - Grid item UUID to update
|
||||
* @param updates - Object containing fields to update
|
||||
* @param editKey - Optional edit key for authorization
|
||||
* @param gridService - Grid service instance
|
||||
* @returns Updated grid item data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await updateGridItem(
|
||||
* 'weapon',
|
||||
* party.id,
|
||||
* gridWeaponId,
|
||||
* { ax1: 10, ax2: 5 },
|
||||
* editKey,
|
||||
* gridService
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export async function updateGridItem(
|
||||
type: GridItemType,
|
||||
partyId: string,
|
||||
gridItemId: string,
|
||||
updates: any,
|
||||
editKey: string | undefined,
|
||||
gridService: GridService
|
||||
): Promise<any> {
|
||||
const methodName = getGridMethodName('update', type)
|
||||
const method = (gridService as any)[methodName]
|
||||
|
||||
return await method.call(gridService, partyId, gridItemId, updates, editKey)
|
||||
}
|
||||
137
src/lib/utils/gridStateUpdater.ts
Normal file
137
src/lib/utils/gridStateUpdater.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* Grid state update utilities
|
||||
* Handles optimistic updates for uncap levels and other grid item properties
|
||||
*/
|
||||
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import type { GridService } from '$lib/services/grid.service'
|
||||
import type { GridItemType, GridCollection } from './gridOperations'
|
||||
import { getCollectionKey } from './gridOperations'
|
||||
|
||||
export interface UncapUpdateParams {
|
||||
gridItemId: string
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic function to update uncap levels for any grid item type
|
||||
* Replaces updateCharacterUncap, updateWeaponUncap, updateSummonUncap
|
||||
*
|
||||
* @param itemType - Type of grid item (character, weapon, or summon)
|
||||
* @param params - Uncap update parameters
|
||||
* @param partyId - Party UUID
|
||||
* @param currentParty - Current party state
|
||||
* @param editKey - Optional edit key for authorization
|
||||
* @param gridService - Grid service instance
|
||||
* @returns Updated party with modified uncap levels
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = await updateGridItemUncap(
|
||||
* 'weapon',
|
||||
* { gridItemId: 'abc123', uncapLevel: 4, transcendenceStep: 1 },
|
||||
* party.id,
|
||||
* party,
|
||||
* editKey,
|
||||
* gridService
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export async function updateGridItemUncap(
|
||||
itemType: GridItemType,
|
||||
params: UncapUpdateParams,
|
||||
partyId: string,
|
||||
currentParty: Party,
|
||||
editKey: string | undefined,
|
||||
gridService: GridService
|
||||
): Promise<Party> {
|
||||
// Get configuration for this item type
|
||||
const config = getGridItemConfig(itemType)
|
||||
|
||||
// Call appropriate service method
|
||||
const response = await config.updateMethod(
|
||||
gridService,
|
||||
partyId,
|
||||
params.gridItemId,
|
||||
params.uncapLevel,
|
||||
params.transcendenceStep,
|
||||
editKey
|
||||
)
|
||||
|
||||
// Extract updated item from response (handle both camelCase and snake_case)
|
||||
const updatedItem = response[config.responseKey] || response[config.snakeCaseKey]
|
||||
if (!updatedItem) return currentParty
|
||||
|
||||
// Update party state optimistically
|
||||
return mergeUpdatedGridItem(currentParty, config.collectionKey, params.gridItemId, {
|
||||
uncapLevel: updatedItem.uncapLevel ?? updatedItem.uncap_level,
|
||||
transcendenceStep: updatedItem.transcendenceStep ?? updatedItem.transcendence_step
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration map for grid item types
|
||||
*/
|
||||
function getGridItemConfig(itemType: GridItemType) {
|
||||
const configs = {
|
||||
character: {
|
||||
updateMethod: (gs: GridService, ...args: any[]) => gs.updateCharacterUncap(...args),
|
||||
responseKey: 'gridCharacter',
|
||||
snakeCaseKey: 'grid_character',
|
||||
collectionKey: 'characters' as GridCollection
|
||||
},
|
||||
weapon: {
|
||||
updateMethod: (gs: GridService, ...args: any[]) => gs.updateWeaponUncap(...args),
|
||||
responseKey: 'gridWeapon',
|
||||
snakeCaseKey: 'grid_weapon',
|
||||
collectionKey: 'weapons' as GridCollection
|
||||
},
|
||||
summon: {
|
||||
updateMethod: (gs: GridService, ...args: any[]) => gs.updateSummonUncap(...args),
|
||||
responseKey: 'gridSummon',
|
||||
snakeCaseKey: 'grid_summon',
|
||||
collectionKey: 'summons' as GridCollection
|
||||
}
|
||||
}
|
||||
|
||||
return configs[itemType]
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges updates into a grid item within party state
|
||||
* Preserves immutability by creating new objects
|
||||
*
|
||||
* @param party - Current party state
|
||||
* @param collection - Collection key (characters, weapons, or summons)
|
||||
* @param itemId - Grid item ID to update
|
||||
* @param updates - Fields to update
|
||||
* @returns New party object with updates applied
|
||||
*/
|
||||
function mergeUpdatedGridItem(
|
||||
party: Party,
|
||||
collection: GridCollection,
|
||||
itemId: string,
|
||||
updates: any
|
||||
): Party {
|
||||
const updatedParty = { ...party }
|
||||
const items = updatedParty[collection]
|
||||
|
||||
if (!items) return party
|
||||
|
||||
const itemIndex = items.findIndex((item: any) => item.id === itemId)
|
||||
if (itemIndex === -1) return party
|
||||
|
||||
const existingItem = items[itemIndex]
|
||||
if (!existingItem) return party
|
||||
|
||||
// Merge updates while preserving essential properties
|
||||
items[itemIndex] = {
|
||||
...existingItem,
|
||||
...updates,
|
||||
id: existingItem.id,
|
||||
position: existingItem.position
|
||||
}
|
||||
|
||||
return updatedParty
|
||||
}
|
||||
66
src/lib/utils/jobSkills.ts
Normal file
66
src/lib/utils/jobSkills.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Job skills transformation utilities
|
||||
* Handles converting between object and array formats for job skills
|
||||
*/
|
||||
|
||||
import type { JobSkill } from '$lib/types/api/entities'
|
||||
|
||||
export interface JobSkillsMap {
|
||||
[slot: string]: JobSkill | null | undefined
|
||||
}
|
||||
|
||||
export interface JobSkillPayload {
|
||||
id: string
|
||||
slot: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts job skills object to API array format
|
||||
* Filters out null/undefined values and adds slot numbers
|
||||
*
|
||||
* @param skillsMap - Object mapping slot numbers to job skills
|
||||
* @returns Array of job skill payloads ready for API submission
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const skillsMap = { '0': skill1, '1': skill2, '2': null }
|
||||
* const payload = transformSkillsToArray(skillsMap)
|
||||
* // Returns: [{ id: 'skill1-id', slot: 0 }, { id: 'skill2-id', slot: 1 }]
|
||||
* ```
|
||||
*/
|
||||
export function transformSkillsToArray(skillsMap: JobSkillsMap): JobSkillPayload[] {
|
||||
return Object.entries(skillsMap)
|
||||
.filter(([_, skill]) => skill !== null && skill !== undefined)
|
||||
.map(([slotKey, skill]) => ({
|
||||
id: skill!.id,
|
||||
slot: parseInt(slotKey)
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a skill in a specific slot (returns new object, immutable)
|
||||
*
|
||||
* @param currentSkills - Current job skills map
|
||||
* @param slot - Slot number to update
|
||||
* @param skill - Job skill to set, or null to remove
|
||||
* @returns New skills map with the update applied
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const updated = updateSkillInSlot(currentSkills, 0, newSkill)
|
||||
* const removed = updateSkillInSlot(currentSkills, 1, null)
|
||||
* ```
|
||||
*/
|
||||
export function updateSkillInSlot(
|
||||
currentSkills: JobSkillsMap,
|
||||
slot: number,
|
||||
skill: JobSkill | null
|
||||
): JobSkillsMap {
|
||||
const updated = { ...currentSkills }
|
||||
if (skill === null) {
|
||||
delete updated[String(slot)]
|
||||
} else {
|
||||
updated[String(slot)] = skill
|
||||
}
|
||||
return updated
|
||||
}
|
||||
Loading…
Reference in a new issue