hensei-web/src/lib/utils/images.ts

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`
}