454 lines
12 KiB
TypeScript
454 lines
12 KiB
TypeScript
/**
|
|
* Centralized image utility system for all game assets
|
|
*
|
|
* Supports both local images (development) and remote AWS S3/CDN images (production)
|
|
* Configure via PUBLIC_SIERO_IMG_URL environment variable
|
|
*/
|
|
|
|
import { getImageBaseUrl } from '$lib/api/adapters/config'
|
|
|
|
export type ResourceType = 'character' | 'weapon' | 'summon'
|
|
export type ImageVariant = 'main' | 'grid' | 'square' | 'detail' | 'base' | 'wide'
|
|
|
|
/**
|
|
* Maps resource type and variant to the correct directory name
|
|
*/
|
|
function getImageDirectory(type: ResourceType, variant: ImageVariant): string {
|
|
// All directories follow the pattern: {type}-{variant}
|
|
return `${type}-${variant}`
|
|
}
|
|
|
|
/**
|
|
* Determines the file extension for a given resource type and variant
|
|
*/
|
|
function getFileExtension(type: ResourceType, variant: ImageVariant): string {
|
|
// PNG variants: character-detail, weapon-base, summon-detail
|
|
if (type === 'character' && variant === 'detail') return '.png'
|
|
if (type === 'weapon' && variant === 'base') return '.png'
|
|
if (type === 'summon' && variant === 'detail') return '.png'
|
|
|
|
// All other variants use JPG
|
|
return '.jpg'
|
|
}
|
|
|
|
/**
|
|
* Gets the base path for images
|
|
* Returns AWS S3/CDN URL if configured, otherwise local /images path
|
|
*/
|
|
export function getBasePath(): string {
|
|
const remoteUrl = getImageBaseUrl()
|
|
return remoteUrl || '/images'
|
|
}
|
|
|
|
/**
|
|
* Gets the placeholder image for a given type and variant
|
|
* Uses AWS S3/CDN in production, local path in development
|
|
*/
|
|
export function getPlaceholderImage(type: ResourceType, variant: ImageVariant): string {
|
|
return `${getBasePath()}/placeholders/placeholder-${type}-${variant}.png`
|
|
}
|
|
|
|
/**
|
|
* Gets a generic placeholder image (weapon-grid variant)
|
|
* Used as fallback for misc image types that don't have specific placeholders
|
|
*/
|
|
export function getGenericPlaceholder(): string {
|
|
return `${getBasePath()}/placeholders/placeholder-weapon-grid.png`
|
|
}
|
|
|
|
/**
|
|
* Calculates the character pose based on uncap level and transcendence
|
|
*/
|
|
export function getCharacterPose(uncapLevel?: number, transcendenceStep?: number): string {
|
|
if (transcendenceStep && transcendenceStep > 0) return '04'
|
|
if (uncapLevel && uncapLevel >= 5) return '03'
|
|
if (uncapLevel && uncapLevel > 2) return '02'
|
|
return '01'
|
|
}
|
|
|
|
/**
|
|
* Main function to get image URL for any resource
|
|
*/
|
|
export function getImageUrl(
|
|
type: ResourceType,
|
|
id: string | number | null | undefined,
|
|
variant: ImageVariant,
|
|
options?: {
|
|
pose?: string | undefined // For character poses
|
|
element?: number | undefined // For element-specific weapon grids
|
|
}
|
|
): string {
|
|
// Return placeholder if no ID
|
|
if (!id) {
|
|
return getPlaceholderImage(type, variant)
|
|
}
|
|
|
|
const directory = getImageDirectory(type, variant)
|
|
const extension = getFileExtension(type, variant)
|
|
const basePath = `${getBasePath()}/${directory}`
|
|
|
|
// Handle character-specific logic
|
|
if (type === 'character') {
|
|
const pose = options?.pose || '01'
|
|
return `${basePath}/${id}_${pose}${extension}`
|
|
}
|
|
|
|
// Handle weapon grid element variants
|
|
if (type === 'weapon' && variant === 'grid' && options?.element && options.element > 0) {
|
|
return `${basePath}/${id}_${options.element}${extension}`
|
|
}
|
|
|
|
// Standard format for weapons and summons
|
|
return `${basePath}/${id}${extension}`
|
|
}
|
|
|
|
// ===== Convenience Functions =====
|
|
|
|
/**
|
|
* Get character image URL
|
|
*/
|
|
export function getCharacterImage(
|
|
id: string | number | null | undefined,
|
|
variant: ImageVariant = 'main',
|
|
pose?: string
|
|
): string {
|
|
return getImageUrl('character', id, variant, { pose })
|
|
}
|
|
|
|
/**
|
|
* Get character detail image (PNG) with pose
|
|
*/
|
|
export function getCharacterDetailImage(
|
|
id: string | number | null | undefined,
|
|
pose?: string
|
|
): string {
|
|
return getImageUrl('character', id, 'detail', { pose })
|
|
}
|
|
|
|
/**
|
|
* Get weapon image URL
|
|
* @param transformation - Optional transformation suffix ('02' for transcendence)
|
|
*/
|
|
export function getWeaponImage(
|
|
id: string | number | null | undefined,
|
|
variant: ImageVariant = 'main',
|
|
element?: number,
|
|
transformation?: string
|
|
): string {
|
|
if (!id) {
|
|
return getPlaceholderImage('weapon', variant)
|
|
}
|
|
|
|
const directory = getImageDirectory('weapon', variant)
|
|
const extension = getFileExtension('weapon', variant)
|
|
const basePath = `${getBasePath()}/${directory}`
|
|
|
|
// Handle element-specific weapon grids
|
|
if (variant === 'grid' && element && element > 0) {
|
|
return `${basePath}/${id}_${element}${extension}`
|
|
}
|
|
|
|
// Handle transformation suffix (transcendence)
|
|
if (transformation) {
|
|
return `${basePath}/${id}_${transformation}${extension}`
|
|
}
|
|
|
|
return `${basePath}/${id}${extension}`
|
|
}
|
|
|
|
/**
|
|
* Get weapon base image (PNG)
|
|
*/
|
|
export function getWeaponBaseImage(
|
|
id: string | number | null | undefined
|
|
): string {
|
|
return getImageUrl('weapon', id, 'base')
|
|
}
|
|
|
|
/**
|
|
* Get summon image URL
|
|
* @param transformation - Optional transformation suffix ('02' for ULB, '03' for transcendence)
|
|
*/
|
|
export function getSummonImage(
|
|
id: string | number | null | undefined,
|
|
variant: ImageVariant = 'main',
|
|
transformation?: string
|
|
): string {
|
|
if (!id) {
|
|
return getPlaceholderImage('summon', variant)
|
|
}
|
|
|
|
const directory = getImageDirectory('summon', variant)
|
|
const extension = getFileExtension('summon', variant)
|
|
const basePath = `${getBasePath()}/${directory}`
|
|
|
|
// Handle transformation suffix (ULB, transcendence)
|
|
if (transformation) {
|
|
return `${basePath}/${id}_${transformation}${extension}`
|
|
}
|
|
|
|
return `${basePath}/${id}${extension}`
|
|
}
|
|
|
|
/**
|
|
* Get summon detail image (PNG)
|
|
*/
|
|
export function getSummonDetailImage(
|
|
id: string | number | null | undefined
|
|
): string {
|
|
return getImageUrl('summon', id, 'detail')
|
|
}
|
|
|
|
/**
|
|
* Get summon wide image
|
|
*/
|
|
export function getSummonWideImage(
|
|
id: string | number | null | undefined
|
|
): string {
|
|
return getImageUrl('summon', id, 'wide')
|
|
}
|
|
|
|
// ===== Special Handlers =====
|
|
|
|
/**
|
|
* Get character image with automatic pose calculation
|
|
*/
|
|
export function getCharacterImageWithPose(
|
|
id: string | number | null | undefined,
|
|
variant: ImageVariant,
|
|
uncapLevel?: number,
|
|
transcendenceStep?: number,
|
|
mainWeaponElement?: number | null,
|
|
partyElement?: number | null
|
|
): string {
|
|
if (!id) {
|
|
return getPlaceholderImage('character', variant)
|
|
}
|
|
|
|
let pose = getCharacterPose(uncapLevel, transcendenceStep)
|
|
|
|
// Special handling for Gran/Djeeta (3030182000)
|
|
if (String(id) === '3030182000') {
|
|
const element = mainWeaponElement || partyElement || 1
|
|
pose = `${pose}_0${element}`
|
|
}
|
|
|
|
return getImageUrl('character', id, variant, { pose })
|
|
}
|
|
|
|
/**
|
|
* Get weapon grid image with element support
|
|
*/
|
|
export function getWeaponGridImage(
|
|
id: string | number | null | undefined,
|
|
element?: number,
|
|
instanceElement?: number
|
|
): string {
|
|
// Handle element-specific weapons (primal weapons)
|
|
if (id && element === 0 && instanceElement) {
|
|
return getImageUrl('weapon', id, 'grid', { element: instanceElement })
|
|
}
|
|
return getImageUrl('weapon', id, 'grid')
|
|
}
|
|
|
|
// ===== Job-Related Images =====
|
|
|
|
/**
|
|
* Get job skill icon URL
|
|
* Uses slug for the image path
|
|
*/
|
|
export function getJobSkillIcon(skill: { imageId?: string; slug?: string } | string | undefined): string {
|
|
if (!skill) return '/images/job-skills/default.png'
|
|
|
|
// Handle string input (backward compatibility)
|
|
if (typeof skill === 'string') {
|
|
return `${getBasePath()}/job-skills/${skill}.png`
|
|
}
|
|
|
|
// Use slug for the image path
|
|
if (skill.slug) {
|
|
return `${getBasePath()}/job-skills/${skill.slug}.png`
|
|
}
|
|
return '/images/job-skills/default.png'
|
|
}
|
|
|
|
/**
|
|
* Get accessory square image URL
|
|
*/
|
|
export function getAccessoryImage(granblueId: string | undefined): string {
|
|
if (!granblueId) return getGenericPlaceholder()
|
|
return `${getBasePath()}/accessory-square/${granblueId}.jpg`
|
|
}
|
|
|
|
// ===== Modification Images =====
|
|
|
|
/**
|
|
* Get awakening image URL
|
|
*/
|
|
export function getAwakeningImage(slug: string | undefined, extension: 'png' | 'jpg' = 'jpg'): string {
|
|
if (!slug) return ''
|
|
return `${getBasePath()}/awakening/${slug}.${extension}`
|
|
}
|
|
|
|
/**
|
|
* Get weapon key image URL
|
|
*/
|
|
export function getWeaponKeyImage(slug: string, element?: number): string {
|
|
const basePath = `${getBasePath()}/weapon-keys`
|
|
|
|
// Check if this key type needs element suffix
|
|
if (element && isElementalWeaponKey(slug)) {
|
|
return `${basePath}/${slug}-${element}.png`
|
|
}
|
|
return `${basePath}/${slug}.png`
|
|
}
|
|
|
|
/**
|
|
* Check if weapon key slug requires element suffix
|
|
*/
|
|
function isElementalWeaponKey(slug: string): boolean {
|
|
const elementalKeys = [
|
|
'elemental-teluma',
|
|
'pendulum',
|
|
'chain-of-causality',
|
|
'ultima'
|
|
]
|
|
return elementalKeys.some((key) => slug.includes(key))
|
|
}
|
|
|
|
/**
|
|
* Get AX skill image URL
|
|
*/
|
|
export function getAxSkillImage(slug: string | undefined): string {
|
|
if (!slug) return ''
|
|
return `${getBasePath()}/ax/${slug}.png`
|
|
}
|
|
|
|
/**
|
|
* Get mastery image URL
|
|
*/
|
|
export function getMasteryImage(slug: string | undefined): string {
|
|
if (!slug) return ''
|
|
return `${getBasePath()}/mastery/${slug}.png`
|
|
}
|
|
|
|
// ===== Label Images =====
|
|
|
|
/**
|
|
* Get element label image URL
|
|
*/
|
|
export function getElementLabelImage(elementName: string): string {
|
|
const capitalizedLabel = elementName.charAt(0).toUpperCase() + elementName.slice(1).toLowerCase()
|
|
return `${getBasePath()}/labels/element/Label_Element_${capitalizedLabel}.png`
|
|
}
|
|
|
|
/**
|
|
* Get proficiency label image URL
|
|
*/
|
|
export function getProficiencyLabelImage(proficiencyName: string): string {
|
|
const capitalizedLabel =
|
|
proficiencyName.charAt(0).toUpperCase() + proficiencyName.slice(1).toLowerCase()
|
|
return `${getBasePath()}/labels/proficiency/Label_Weapon_${capitalizedLabel}.png`
|
|
}
|
|
|
|
/**
|
|
* Get race label image URL
|
|
*/
|
|
export function getRaceLabelImage(raceName: string): string {
|
|
return `${getBasePath()}/labels/race/Label_Race_${raceName}.png`
|
|
}
|
|
|
|
/**
|
|
* Get gender label image URL
|
|
*/
|
|
export function getGenderLabelImage(genderLabel: string): string {
|
|
return `${getBasePath()}/labels/gender/Label_Gender_${genderLabel.replace('/', '_')}.png`
|
|
}
|
|
|
|
// ===== Element Icons =====
|
|
|
|
/**
|
|
* Get element icon image URL (for select dropdowns, etc.)
|
|
*/
|
|
export function getElementIcon(element: number): string {
|
|
const elementNames: Record<number, string> = {
|
|
1: 'wind',
|
|
2: 'fire',
|
|
3: 'water',
|
|
4: 'earth',
|
|
5: 'dark',
|
|
6: 'light'
|
|
}
|
|
const name = elementNames[element] || 'none'
|
|
return `${getBasePath()}/elements/element-${name}.png`
|
|
}
|
|
|
|
// ===== Artifact Images =====
|
|
|
|
export type ArtifactImageVariant = 'square' | 'wide'
|
|
|
|
/**
|
|
* Get artifact image URL
|
|
* @param variant - 'square' for thumbnails/icons, 'wide' for grid-sized display
|
|
*/
|
|
export function getArtifactImage(
|
|
granblueId: string | number | null | undefined,
|
|
variant: ArtifactImageVariant = 'square'
|
|
): string {
|
|
if (!granblueId) return getGenericPlaceholder()
|
|
const directory = `artifact-${variant}`
|
|
return `${getBasePath()}/${directory}/${granblueId}.jpg`
|
|
}
|
|
|
|
// ===== Game CDN Images =====
|
|
// For new items not yet in our AWS CDN (used in batch import)
|
|
|
|
const GAME_CDN_BASE = 'https://prd-game-a-granbluefantasy.akamaized.net/assets_en/img/sp/assets'
|
|
|
|
/**
|
|
* Get character image from the game CDN
|
|
* Used for batch imports where images aren't yet in AWS
|
|
*/
|
|
export function getGameCdnCharacterImage(
|
|
id: string | number | null | undefined,
|
|
pose: string = '01'
|
|
): string {
|
|
if (!id) return getPlaceholderImage('character', 'square')
|
|
return `${GAME_CDN_BASE}/npc/s/${id}_${pose}.jpg`
|
|
}
|
|
|
|
/**
|
|
* Get weapon image from the game CDN
|
|
* Used for batch imports where images aren't yet in AWS
|
|
*/
|
|
export function getGameCdnWeaponImage(id: string | number | null | undefined): string {
|
|
if (!id) return getPlaceholderImage('weapon', 'square')
|
|
return `${GAME_CDN_BASE}/weapon/s/${id}.jpg`
|
|
}
|
|
|
|
/**
|
|
* Get summon image from the game CDN
|
|
* Used for batch imports where images aren't yet in AWS
|
|
*/
|
|
export function getGameCdnSummonImage(id: string | number | null | undefined): string {
|
|
if (!id) return getPlaceholderImage('summon', 'square')
|
|
return `${GAME_CDN_BASE}/summon/s/${id}.jpg`
|
|
}
|
|
|
|
// ===== Other Game Images =====
|
|
|
|
/**
|
|
* Get guidebook image URL
|
|
*/
|
|
export function getGuidebookImage(granblueId: string | number | undefined): string {
|
|
if (!granblueId) return getGenericPlaceholder()
|
|
return `${getBasePath()}/guidebooks/book_${granblueId}.png`
|
|
}
|
|
|
|
/**
|
|
* Get raid image URL
|
|
*/
|
|
export function getRaidImage(slug: string | undefined): string {
|
|
if (!slug) return getGenericPlaceholder()
|
|
return `${getBasePath()}/raids/${slug}.png`
|
|
}
|