/** * Entity Adapter * * Handles read-only access to canonical game data (weapons, characters, summons). * This data represents the official game information that users reference * but cannot modify. * * @module adapters/entity */ import { BaseAdapter } from './base.adapter' import type { AdapterOptions } from './types' import { DEFAULT_ADAPTER_CONFIG } from './config' /** * Canonical weapon data from the game */ export interface Weapon { id: string granblueId: string name: { en?: string ja?: string } rarity: number element: number proficiency: number series?: number weaponType?: number minHp?: number maxHp?: number minAttack?: number maxAttack?: number flbHp?: number flbAttack?: number ulbHp?: number ulbAttack?: number transcendenceHp?: number transcendenceAttack?: number hp?: { minHp?: number maxHp?: number maxHpFlb?: number maxHpUlb?: number } atk?: { minAtk?: number maxAtk?: number maxAtkFlb?: number maxAtkUlb?: number } uncap?: { flb?: boolean ulb?: boolean transcendence?: boolean } maxLevel?: number skillLevelCap?: number weapon_skills?: Array<{ name?: string description?: string }> awakenings?: Array<{ id: string name: Record level: number }> } /** * Canonical character data from the game */ export interface Character { id: string granblueId: string characterId?: number name: { en?: string ja?: string } rarity: number element: number gender?: number proficiency?: number[] proficiency1?: number proficiency2?: number series?: number race?: number[] hp?: { minHp?: number maxHp?: number maxHpFlb?: number } atk?: { minAtk?: number maxAtk?: number maxAtkFlb?: number } uncap?: { flb?: boolean ulb?: boolean transcendence?: boolean } special?: boolean seasonalId?: string awakenings?: Array<{ id: string name: Record level: number }> } /** * Weapon key data for customizing weapons */ export interface WeaponKey { id: string granblue_id: number name: { en: string ja: string } slug: string series: number[] slot: number group: number order: number } /** * Query parameters for fetching weapon keys */ export interface WeaponKeyQueryParams { series?: number slot?: number group?: number } /** * Canonical summon data from the game */ export interface Summon { id: string granblueId: string name: { en?: string ja?: string } rarity: number element: number series?: number minHp?: number maxHp?: number minAttack?: number maxAttack?: number flbHp?: number flbAttack?: number ulbHp?: number ulbAttack?: number transcendenceHp?: number transcendenceAttack?: number hp?: { minHp?: number maxHp?: number maxHpFlb?: number maxHpUlb?: number maxHpXlb?: number } atk?: { minAtk?: number maxAtk?: number maxAtkFlb?: number maxAtkUlb?: number maxAtkXlb?: number } uncap?: { flb?: boolean ulb?: boolean transcendence?: boolean } subaura?: boolean cooldown?: number callName?: string callDescription?: string auraName?: string auraDescription?: string subAuraName?: string subAuraDescription?: string } /** * Response from character granblue_id validation */ export interface CharacterValidationResult { valid: boolean granblueId: string existsInDb: boolean error?: string imageUrls?: { main?: string grid?: string square?: string } } /** * Payload for creating a new character */ export interface CreateCharacterPayload { granblue_id: string name_en: string name_jp?: string character_id?: number[] // Array for dual/trio units rarity?: number element?: number race1?: number | null race2?: number | null gender?: number proficiency1?: number proficiency2?: number min_hp?: number max_hp?: number max_hp_flb?: number max_hp_ulb?: number min_atk?: number max_atk?: number max_atk_flb?: number max_atk_ulb?: number base_da?: number base_ta?: number ougi_ratio?: number ougi_ratio_flb?: number flb?: boolean ulb?: boolean special?: boolean release_date?: string | null flb_date?: string | null ulb_date?: string | null wiki_en?: string wiki_ja?: string gamewith?: string kamigame?: string nicknames_en?: string[] nicknames_jp?: string[] } /** * Response from character image download status */ export interface CharacterDownloadStatus { status: 'queued' | 'processing' | 'completed' | 'failed' | 'not_found' progress?: number imagesDownloaded?: number imagesTotal?: number error?: string characterId?: string granblueId?: string images?: Record updatedAt?: string } /** * Response from summon granblue_id validation */ export interface SummonValidationResult { valid: boolean granblueId: string existsInDb: boolean error?: string imageUrls?: { main?: string grid?: string square?: string } } /** * Payload for creating a new summon * Note: Frontend uses "transcendence" but API expects "xlb" for stats */ export interface CreateSummonPayload { granblue_id: string name_en: string name_jp?: string summon_id?: string rarity?: number element?: number series?: string min_hp?: number max_hp?: number max_hp_flb?: number max_hp_ulb?: number max_hp_xlb?: number // transcendence HP min_atk?: number max_atk?: number max_atk_flb?: number max_atk_ulb?: number max_atk_xlb?: number // transcendence ATK max_level?: number flb?: boolean ulb?: boolean transcendence?: boolean subaura?: boolean limit?: boolean release_date?: string | null flb_date?: string | null ulb_date?: string | null transcendence_date?: string | null wiki_en?: string wiki_ja?: string gamewith?: string kamigame?: string nicknames_en?: string[] nicknames_jp?: string[] } /** * Response from summon image download status */ export interface SummonDownloadStatus { status: 'queued' | 'processing' | 'completed' | 'failed' | 'not_found' progress?: number imagesDownloaded?: number imagesTotal?: number error?: string summonId?: string granblueId?: string images?: Record updatedAt?: string } /** * Response from weapon granblue_id validation */ export interface WeaponValidationResult { valid: boolean granblueId: string existsInDb: boolean error?: string imageUrls?: { main?: string grid?: string square?: string } } /** * Payload for creating a new weapon * Note: Frontend uses "transcendence" but API expects "xlb" for stats */ export interface CreateWeaponPayload { granblue_id: string name_en: string name_jp?: string rarity?: number element?: number proficiency?: number series?: number new_series?: number min_hp?: number max_hp?: number max_hp_flb?: number max_hp_ulb?: number min_atk?: number max_atk?: number max_atk_flb?: number max_atk_ulb?: number max_level?: number max_skill_level?: number max_awakening_level?: number flb?: boolean ulb?: boolean transcendence?: boolean extra?: boolean limit?: boolean ax?: boolean release_date?: string | null flb_date?: string | null ulb_date?: string | null transcendence_date?: string | null wiki_en?: string wiki_ja?: string gamewith?: string kamigame?: string recruits?: string | null // Character ID reference nicknames_en?: string[] nicknames_jp?: string[] } /** * Response from weapon image download status */ export interface WeaponDownloadStatus { status: 'queued' | 'processing' | 'completed' | 'failed' | 'not_found' progress?: number imagesDownloaded?: number imagesTotal?: number error?: string weaponId?: string granblueId?: string images?: Record updatedAt?: string } /** * Raw data response from /raw endpoint */ export interface EntityRawData { wikiRaw: string | null gameRawEn: Record | null gameRawJp: Record | null } /** * Suggestions for character fields parsed from wiki data */ export interface CharacterSuggestions { nameEn?: string nameJp?: string granblueId?: string characterId?: number[] rarity?: number element?: number gender?: number proficiency1?: number proficiency2?: number race1?: number race2?: number minHp?: number maxHp?: number maxHpFlb?: number minAtk?: number maxAtk?: number maxAtkFlb?: number flb?: boolean ulb?: boolean releaseDate?: string flbDate?: string ulbDate?: string gamewith?: string kamigame?: string } /** * Suggestions for weapon fields parsed from wiki data */ export interface WeaponSuggestions { nameEn?: string nameJp?: string granblueId?: string rarity?: number element?: number proficiency?: number minHp?: number maxHp?: number maxHpFlb?: number minAtk?: number maxAtk?: number maxAtkFlb?: number flb?: boolean ulb?: boolean releaseDate?: string flbDate?: string ulbDate?: string gamewith?: string kamigame?: string recruits?: string } /** * Suggestions for summon fields parsed from wiki data */ export interface SummonSuggestions { nameEn?: string nameJp?: string granblueId?: string rarity?: number element?: number minHp?: number maxHp?: number maxHpFlb?: number minAtk?: number maxAtk?: number maxAtkFlb?: number flb?: boolean ulb?: boolean subaura?: boolean releaseDate?: string flbDate?: string ulbDate?: string gamewith?: string kamigame?: string } /** * Result from batch_preview for a single wiki page */ export interface BatchPreviewResult { wikiPage: string status: 'success' | 'error' granblueId?: string wikiRaw?: string suggestions?: T imageStatus?: 'pending' | 'exists' | 'error' | 'no_id' error?: string redirectedFrom?: string } /** * Response from batch_preview endpoint */ export interface BatchPreviewResponse { results: BatchPreviewResult[] } /** * Entity adapter for accessing canonical game data */ export class EntityAdapter extends BaseAdapter { /** * Gets canonical weapon data by ID */ async getWeapon(id: string): Promise { return this.request(`/weapons/${id}`, { method: 'GET', cacheTTL: 600000 // Cache for 10 minutes }) } /** * Gets canonical character data by ID */ async getCharacter(id: string): Promise { return this.request(`/characters/${id}`, { method: 'GET', cacheTTL: 600000 // Cache for 10 minutes }) } /** * Gets related characters (same character_id) for a given character */ async getRelatedCharacters(id: string): Promise { return this.request(`/characters/${id}/related`, { method: 'GET', cacheTTL: 600000 // Cache for 10 minutes }) } /** * Gets canonical summon data by ID */ async getSummon(id: string): Promise { return this.request(`/summons/${id}`, { method: 'GET', cacheTTL: 600000 // Cache for 10 minutes }) } /** * Batch fetch multiple weapons */ async getWeapons(ids: string[]): Promise { // Fetch in parallel with individual caching const promises = ids.map(id => this.getWeapon(id)) return Promise.all(promises) } /** * Batch fetch multiple characters */ async getCharacters(ids: string[]): Promise { const promises = ids.map(id => this.getCharacter(id)) return Promise.all(promises) } /** * Batch fetch multiple summons */ async getSummons(ids: string[]): Promise { const promises = ids.map(id => this.getSummon(id)) return Promise.all(promises) } /** * Gets weapon keys with optional filtering */ async getWeaponKeys(params?: WeaponKeyQueryParams): Promise { const searchParams = new URLSearchParams() if (params?.series !== undefined) searchParams.set('series', String(params.series)) if (params?.slot !== undefined) searchParams.set('slot', String(params.slot)) if (params?.group !== undefined) searchParams.set('group', String(params.group)) const queryString = searchParams.toString() const url = queryString ? `/weapon_keys?${queryString}` : '/weapon_keys' return this.request(url, { method: 'GET', cacheTTL: 3600000 // Cache for 1 hour - weapon keys rarely change }) } /** * Clears entity cache */ clearEntityCache(type?: 'weapons' | 'characters' | 'summons' | 'weapon_keys') { if (type) { this.clearCache(`/${type}`) } else { // Clear all entity caches this.clearCache('/weapons') this.clearCache('/characters') this.clearCache('/summons') this.clearCache('/weapon_keys') } } // ============================================ // Character Creation & Image Download Methods // ============================================ /** * Validates a character granblue_id by checking if images exist on GBF servers * Requires editor role (>= 7) */ async validateCharacterGranblueId(granblueId: string): Promise { const response = await this.request<{ valid: boolean granblue_id: string exists_in_db: boolean error?: string image_urls?: { main?: string grid?: string square?: string } }>(`/characters/validate/${granblueId}`, { method: 'GET' }) return { valid: response.valid, granblueId: response.granblue_id, existsInDb: response.exists_in_db, error: response.error, imageUrls: response.image_urls } } /** * Creates a new character record * Requires editor role (>= 7) */ async createCharacter(payload: CreateCharacterPayload): Promise { return this.request('/characters', { method: 'POST', body: { character: payload } }) } /** * Updates an existing character record * Requires editor role (>= 7) */ async updateCharacter(id: string, payload: Partial): Promise { const result = await this.request(`/characters/${id}`, { method: 'PATCH', body: { character: payload } }) // Invalidate cache for this character this.clearCache(`/characters/${id}`) return result } /** * Triggers async image download for a character * Requires editor role (>= 7) */ async downloadCharacterImages( characterId: string, options?: { force?: boolean; size?: 'all' | string } ): Promise<{ status: string; characterId: string; message: string }> { return this.request(`/characters/${characterId}/download_images`, { method: 'POST', body: { options } }) } /** * Downloads a single image for a character (synchronous) * Requires editor role (>= 7) * @param characterId - Character database ID * @param size - Image size variant (main, grid, square, detail) * @param transformation - Pose variant (01=Base, 02=MLB, 03=FLB, 04=Transcendence) * @param force - Force re-download even if image exists */ async downloadCharacterImage( characterId: string, size: string, transformation?: string, force?: boolean ): Promise<{ success: boolean; error?: string }> { return this.request(`/characters/${characterId}/download_image`, { method: 'POST', body: { size, transformation, force } }) } /** * Gets the status of an ongoing character image download * Requires editor role (>= 7) */ async getCharacterDownloadStatus(characterId: string): Promise { const response = await this.request<{ status: string progress?: number images_downloaded?: number images_total?: number error?: string character_id?: string granblue_id?: string images?: Record updated_at?: string }>(`/characters/${characterId}/download_status`, { method: 'GET' }) return { status: response.status as CharacterDownloadStatus['status'], progress: response.progress, imagesDownloaded: response.images_downloaded, imagesTotal: response.images_total, error: response.error, characterId: response.character_id, granblueId: response.granblue_id, images: response.images, updatedAt: response.updated_at } } // ============================================ // Summon Creation & Image Download Methods // ============================================ /** * Validates a summon granblue_id by checking if images exist on GBF servers * Requires editor role (>= 7) */ async validateSummonGranblueId(granblueId: string): Promise { const response = await this.request<{ valid: boolean granblue_id: string exists_in_db: boolean error?: string image_urls?: { main?: string grid?: string square?: string } }>(`/summons/validate/${granblueId}`, { method: 'GET' }) return { valid: response.valid, granblueId: response.granblue_id, existsInDb: response.exists_in_db, error: response.error, imageUrls: response.image_urls } } /** * Creates a new summon record * Requires editor role (>= 7) */ async createSummon(payload: CreateSummonPayload): Promise { return this.request('/summons', { method: 'POST', body: { summon: payload } }) } /** * Updates an existing summon record * Requires editor role (>= 7) */ async updateSummon(id: string, payload: Partial): Promise { const result = await this.request(`/summons/${id}`, { method: 'PATCH', body: { summon: payload } }) // Invalidate cache for this summon this.clearCache(`/summons/${id}`) return result } /** * Triggers async image download for a summon * Requires editor role (>= 7) */ async downloadSummonImages( summonId: string, options?: { force?: boolean; size?: 'all' | string } ): Promise<{ status: string; summonId: string; message: string }> { return this.request(`/summons/${summonId}/download_images`, { method: 'POST', body: { options } }) } /** * Downloads a single image for a summon (synchronous) * Requires editor role (>= 7) * @param summonId - Summon database ID * @param size - Image size variant (main, grid, wide, square, detail) * @param transformation - Pose variant (empty=Base, 02=ULB, 03=Trans1, 04=Trans5) * @param force - Force re-download even if image exists */ async downloadSummonImage( summonId: string, size: string, transformation?: string, force?: boolean ): Promise<{ success: boolean; error?: string }> { return this.request(`/summons/${summonId}/download_image`, { method: 'POST', body: { size, transformation, force } }) } /** * Gets the status of an ongoing summon image download * Requires editor role (>= 7) */ async getSummonDownloadStatus(summonId: string): Promise { const response = await this.request<{ status: string progress?: number images_downloaded?: number images_total?: number error?: string summon_id?: string granblue_id?: string images?: Record updated_at?: string }>(`/summons/${summonId}/download_status`, { method: 'GET' }) return { status: response.status as SummonDownloadStatus['status'], progress: response.progress, imagesDownloaded: response.images_downloaded, imagesTotal: response.images_total, error: response.error, summonId: response.summon_id, granblueId: response.granblue_id, images: response.images, updatedAt: response.updated_at } } // ============================================ // Weapon Creation & Image Download Methods // ============================================ /** * Validates a weapon granblue_id by checking if images exist on GBF servers * Requires editor role (>= 7) */ async validateWeaponGranblueId(granblueId: string): Promise { const response = await this.request<{ valid: boolean granblue_id: string exists_in_db: boolean error?: string image_urls?: { main?: string grid?: string square?: string } }>(`/weapons/validate/${granblueId}`, { method: 'GET' }) return { valid: response.valid, granblueId: response.granblue_id, existsInDb: response.exists_in_db, error: response.error, imageUrls: response.image_urls } } /** * Creates a new weapon record * Requires editor role (>= 7) */ async createWeapon(payload: CreateWeaponPayload): Promise { return this.request('/weapons', { method: 'POST', body: { weapon: payload } }) } /** * Updates an existing weapon record * Requires editor role (>= 7) */ async updateWeapon(id: string, payload: Partial): Promise { const result = await this.request(`/weapons/${id}`, { method: 'PATCH', body: { weapon: payload } }) // Invalidate cache for this weapon this.clearCache(`/weapons/${id}`) return result } /** * Triggers async image download for a weapon * Requires editor role (>= 7) */ async downloadWeaponImages( weaponId: string, options?: { force?: boolean; size?: 'all' | string } ): Promise<{ status: string; weaponId: string; message: string }> { return this.request(`/weapons/${weaponId}/download_images`, { method: 'POST', body: { options } }) } /** * Downloads a single image for a weapon (synchronous) * Requires editor role (>= 7) * @param weaponId - Weapon database ID * @param size - Image size variant (main, grid, square, base) * @param transformation - Pose variant (empty=Base, 02=Trans1, 03=Trans5) * @param force - Force re-download even if image exists */ async downloadWeaponImage( weaponId: string, size: string, transformation?: string, force?: boolean ): Promise<{ success: boolean; error?: string }> { return this.request(`/weapons/${weaponId}/download_image`, { method: 'POST', body: { size, transformation, force } }) } /** * Gets the status of an ongoing weapon image download * Requires editor role (>= 7) */ async getWeaponDownloadStatus(weaponId: string): Promise { const response = await this.request<{ status: string progress?: number images_downloaded?: number images_total?: number error?: string weapon_id?: string granblue_id?: string images?: Record updated_at?: string }>(`/weapons/${weaponId}/download_status`, { method: 'GET' }) return { status: response.status as WeaponDownloadStatus['status'], progress: response.progress, imagesDownloaded: response.images_downloaded, imagesTotal: response.images_total, error: response.error, weaponId: response.weapon_id, granblueId: response.granblue_id, images: response.images, updatedAt: response.updated_at } } // ============================================ // Raw Data Methods (for database viewing) // ============================================ /** * Gets raw wiki and game data for a character * This data is fetched separately to avoid bloating regular entity responses * Note: BaseAdapter.request() automatically transforms snake_case to camelCase */ async getCharacterRawData(id: string): Promise { // Response keys are already camelCase after BaseAdapter.transformResponse() const response = await this.request(`/characters/${id}/raw`, { method: 'GET' }) return response } /** * Gets raw wiki and game data for a weapon * This data is fetched separately to avoid bloating regular entity responses * Note: BaseAdapter.request() automatically transforms snake_case to camelCase */ async getWeaponRawData(id: string): Promise { // Response keys are already camelCase after BaseAdapter.transformResponse() const response = await this.request(`/weapons/${id}/raw`, { method: 'GET' }) return response } /** * Gets raw wiki and game data for a summon * This data is fetched separately to avoid bloating regular entity responses * Note: BaseAdapter.request() automatically transforms snake_case to camelCase */ async getSummonRawData(id: string): Promise { // Response keys are already camelCase after BaseAdapter.transformResponse() const response = await this.request(`/summons/${id}/raw`, { method: 'GET' }) return response } // ============================================ // Wiki Fetch Methods (editor-only) // ============================================ /** * Fetches and stores wiki data for a character * Requires editor role (>= 7) */ async fetchCharacterWiki(id: string): Promise { return this.request(`/characters/${id}/fetch_wiki`, { method: 'POST' }) } /** * Fetches and stores wiki data for a weapon * Requires editor role (>= 7) */ async fetchWeaponWiki(id: string): Promise { return this.request(`/weapons/${id}/fetch_wiki`, { method: 'POST' }) } /** * Fetches and stores wiki data for a summon * Requires editor role (>= 7) */ async fetchSummonWiki(id: string): Promise { return this.request(`/summons/${id}/fetch_wiki`, { method: 'POST' }) } // ============================================ // Batch Preview Methods (for batch import) // ============================================ /** * Fetches wiki data and suggestions for multiple character wiki pages * Requires editor role (>= 7) * @param wikiPages - Array of wiki page names (max 10) */ async batchPreviewCharacters( wikiPages: string[] ): Promise> { return this.request>('/characters/batch_preview', { method: 'POST', body: { wiki_pages: wikiPages } }) } /** * Fetches wiki data and suggestions for multiple weapon wiki pages * Requires editor role (>= 7) * @param wikiPages - Array of wiki page names (max 10) */ async batchPreviewWeapons( wikiPages: string[] ): Promise> { return this.request>('/weapons/batch_preview', { method: 'POST', body: { wiki_pages: wikiPages } }) } /** * Fetches wiki data and suggestions for multiple summon wiki pages * Requires editor role (>= 7) * @param wikiPages - Array of wiki page names (max 10) */ async batchPreviewSummons( wikiPages: string[] ): Promise> { return this.request>('/summons/batch_preview', { method: 'POST', body: { wiki_pages: wikiPages } }) } } /** * Default entity adapter instance */ export const entityAdapter = new EntityAdapter(DEFAULT_ADAPTER_CONFIG)