hensei-web/src/lib/api/adapters/party.adapter.ts

453 lines
10 KiB
TypeScript

/**
* Party Adapter
*
* Handles all party-related API operations including CRUD, grids, and remixing.
* Provides a clean interface for party management with automatic
* request handling, caching, and error management.
*
* @module adapters/party
*/
import { BaseAdapter } from './base.adapter'
import type { AdapterOptions, PaginatedResponse } from './types'
import { DEFAULT_ADAPTER_CONFIG } from './config'
import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
import type { PartyShare } from '$lib/types/api/partyShare'
/**
* Parameters for creating a new party
*/
export interface CreatePartyParams {
name?: string | undefined
description?: string | undefined
visibility?: 'public' | 'private' | 'unlisted' | undefined
jobId?: string | undefined
raidId?: string | null | undefined
guidebookId?: string | undefined
extras?: Record<string, any> | undefined
}
/**
* Parameters for updating a party
*/
export interface UpdatePartyParams extends CreatePartyParams {
/** Party UUID (required for API update) */
id: string
/** Party shortcode (for cache invalidation) */
shortcode: string
// Battle settings
fullAuto?: boolean
autoGuard?: boolean
autoSummon?: boolean
chargeAttack?: boolean
// Performance metrics (null to clear)
clearTime?: number | null
buttonCount?: number | null
chainCount?: number | null
summonCount?: number | null
// Video (null to clear)
videoUrl?: string | null
// Raid (null to clear)
raidId?: string | null
}
/**
* Parameters for listing user parties
*/
export interface ListUserPartiesParams {
username: string
page?: number
per?: number
visibility?: 'public' | 'private' | 'unlisted' | 'all'
raidId?: string
characterId?: string
weaponId?: string
summonId?: string
}
/**
* Parameters for listing parties by raid
*/
export interface ListRaidPartiesParams {
raidId: string
page?: number
per?: number
element?: number
fullAuto?: boolean
autoGuard?: boolean
chargeAttack?: boolean
}
/**
* Grid operation for batch updates
*/
export interface GridOperation {
type: 'move' | 'swap' | 'remove'
entity: 'weapon' | 'character' | 'summon'
id?: string
sourceId?: string
targetId?: string
position?: number
container?: string
}
/**
* Options for grid update operation
*/
export interface GridUpdateOptions {
maintainCharacterSequence?: boolean
validateBeforeExecute?: boolean
}
/**
* Response from grid update operation
*/
export interface GridUpdateResponse {
party: Party
operationsApplied: number
changes: Array<{
entity: string
id: string
action: string
from?: number
to?: number
with?: string
}>
}
/**
* Party adapter for managing parties and their grids
*/
export class PartyAdapter extends BaseAdapter {
constructor(options?: AdapterOptions) {
super(options)
// Temporarily disable cache until cache invalidation is fixed
this.disableCache = true
}
/**
* Creates a new party
*/
async create(params: CreatePartyParams): Promise<Party> {
const response = await this.request<{ party: Party }>('/parties', {
method: 'POST',
body: {
party: params
}
})
return response.party
}
/**
* Gets a party by shortcode
*/
async getByShortcode(shortcode: string): Promise<Party> {
const response = await this.request<{ party: Party }>(`/parties/${shortcode}`, {
cacheTTL: 60000 // Cache for 1 minute
})
return response.party
}
/**
* Updates a party
* Note: API expects UUID for update, not shortcode
*/
async update(params: UpdatePartyParams): Promise<Party> {
const { id, shortcode, ...updateParams } = params
const response = await this.request<{ party: Party }>(`/parties/${id}`, {
method: 'PATCH',
body: {
party: updateParams
}
})
return response.party
}
/**
* Deletes a party
* @param id - The party's UUID (not shortcode - API requires UUID for delete)
*/
async delete(id: string): Promise<void> {
return this.request<void>(`/parties/${id}`, {
method: 'DELETE'
})
}
/**
* Creates a remix (copy) of an existing party
*/
async remix(shortcode: string): Promise<Party> {
const response = await this.request<{ party: Party }>(`/parties/${shortcode}/remix`, {
method: 'POST'
})
return response.party
}
/**
* Lists all public parties (explore page)
*/
async list(params: { page?: number; per?: number } = {}): Promise<PaginatedResponse<Party>> {
const response = await this.request<{
results: Party[]
meta?: {
count?: number
totalPages?: number
perPage?: number
}
}>('/parties', {
method: 'GET',
query: params,
cacheTTL: 30000 // Cache for 30 seconds
})
return {
results: response.results,
page: params.page || 1,
total: response.meta?.count || 0,
totalPages: response.meta?.totalPages || 1,
perPage: response.meta?.perPage || 20
}
}
/**
* Lists parties for a specific user
*/
async listUserParties(params: ListUserPartiesParams): Promise<PaginatedResponse<Party>> {
const { username, ...queryParams } = params
return this.request<PaginatedResponse<Party>>(`/users/${username}/parties`, {
method: 'GET',
query: queryParams,
cacheTTL: 30000 // Cache for 30 seconds
})
}
/**
* Lists public parties for a specific raid
*/
async listRaidParties(params: ListRaidPartiesParams): Promise<PaginatedResponse<Party>> {
const { raidId, element, fullAuto, autoGuard, chargeAttack, ...rest } = params
// Build query with raid filter and convert booleans to API format
const query: Record<string, unknown> = {
...rest,
raid: raidId
}
if (element !== undefined && element >= 0) query.element = element
if (fullAuto !== undefined) query.full_auto = fullAuto ? 1 : 0
if (autoGuard !== undefined) query.auto_guard = autoGuard ? 1 : 0
if (chargeAttack !== undefined) query.charge_attack = chargeAttack ? 1 : 0
const response = await this.request<{
results: Party[]
meta?: {
count?: number
totalPages?: number
perPage?: number
}
}>('/parties', {
method: 'GET',
query,
cacheTTL: 30000
})
return {
results: response.results,
page: params.page || 1,
total: response.meta?.count || 0,
totalPages: response.meta?.totalPages || 1,
perPage: response.meta?.perPage || 20
}
}
/**
* Performs atomic batch grid updates
* Supports move, swap, and remove operations on grid items
*/
async gridUpdate(
shortcode: string,
operations: GridOperation[],
options?: GridUpdateOptions
): Promise<GridUpdateResponse> {
return this.request(`/parties/${shortcode}/grid_update`, {
method: 'POST',
body: {
operations,
options
}
})
}
/**
* Updates the job for a party
*/
async updateJob(shortcode: string, jobId: string): Promise<Party> {
return this.request<Party>(`/parties/${shortcode}/jobs`, {
method: 'PUT',
body: {
party: {
job_id: jobId
}
}
})
}
/**
* Updates job skills for a party
*/
async updateJobSkills(
partyId: string,
skills: Array<{ id: string; slot: number }>
): Promise<Party> {
console.log('[updateJobSkills] Input skills array:', skills)
// Convert skills array to Rails expected format
// Rails has skill0_id (main, locked), skill1_id, skill2_id, skill3_id
// Only include skills that have actual IDs - don't send null values
// as Rails will try to validate them
const party: Record<string, string> = {}
// Set the provided skills - slot number maps directly to skill{N}_id
skills.forEach((skill) => {
// Only set editable slots (1, 2, 3) and only if skill has an ID
if (skill.slot >= 1 && skill.slot <= 3 && skill.id) {
party[`skill${skill.slot}_id`] = skill.id
}
})
const requestBody = {
party
}
console.log('[updateJobSkills] Sending to server:', {
url: `/parties/${partyId}/job_skills`,
body: requestBody
})
return this.request<Party>(`/parties/${partyId}/job_skills`, {
method: 'PUT',
body: requestBody
})
}
/**
* Updates the accessory for a party
*/
async updateAccessory(partyId: string, accessoryId: string): Promise<Party> {
return this.request<Party>(`/parties/${partyId}/accessory`, {
method: 'PUT',
body: { accessory_id: accessoryId }
})
}
/**
* Removes a job skill from a party
*/
async removeJobSkill(partyId: string, skillSlot: number): Promise<Party> {
return this.request<Party>(`/parties/${partyId}/job_skills`, {
method: 'DELETE',
body: {
party: {
skill_position: skillSlot
}
}
})
}
/**
* Gets party preview image
*/
async getPreview(shortcode: string): Promise<Blob> {
return this.request<Blob>(`/parties/${shortcode}/preview`, {
method: 'GET',
headers: {
Accept: 'image/png'
}
})
}
/**
* Gets party preview status
*/
async getPreviewStatus(shortcode: string): Promise<{
state: string
generatedAt?: string
readyForPreview: boolean
}> {
return this.request(`/parties/${shortcode}/preview_status`, {
method: 'GET'
})
}
/**
* Regenerates party preview
*/
async regeneratePreview(shortcode: string): Promise<{ status: string }> {
return this.request(`/parties/${shortcode}/regenerate_preview`, {
method: 'POST'
})
}
/**
* Favorite a party
*/
async favorite(shortcode: string): Promise<void> {
await this.request(`/parties/${shortcode}/favorite`, {
method: 'POST'
})
// Clear cache for the party to reflect updated state
this.clearCache(`/parties/${shortcode}`)
}
/**
* Unfavorite a party
*/
async unfavorite(shortcode: string): Promise<void> {
await this.request(`/parties/${shortcode}/unfavorite`, {
method: 'DELETE'
})
// Clear cache for the party to reflect updated state
this.clearCache(`/parties/${shortcode}`)
}
/**
* Share a party with the current user's crew
* @param partyId - The party's UUID
*/
async shareWithCrew(partyId: string): Promise<PartyShare> {
const response = await this.request<{ share: PartyShare }>(`/parties/${partyId}/shares`, {
method: 'POST'
})
return response.share
}
/**
* Remove a party share
* @param partyId - The party's UUID
* @param shareId - The share's UUID to remove
*/
async removeShare(partyId: string, shareId: string): Promise<void> {
await this.request(`/parties/${partyId}/shares/${shareId}`, {
method: 'DELETE'
})
}
/**
* Clears the cache for party-related data
*/
clearPartyCache(shortcode?: string) {
if (shortcode) {
// Clear specific party cache
this.clearCache(`/parties/${shortcode}`)
} else {
// Clear all party and user caches
this.clearCache('/parties')
this.clearCache('/users')
}
}
}
/**
* Default party adapter instance
*/
export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG)