add utility functions for party component
- extractErrorMessage: handle nested api error structures - transformSkillsToArray: convert job skills to api format - findNextEmptySlot: find available positions in grids
This commit is contained in:
parent
f9bb43f214
commit
112e8c39a9
3 changed files with 229 additions and 0 deletions
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)
|
||||||
|
}
|
||||||
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