/** * Job Utility Functions * * Helper functions for job-related operations including * image URL generation, tier naming, and validation. */ import type { Job, JobSkill } from '$lib/types/api/entities' import type { JobSkillList } from '$lib/types/api/party' import { getImageBaseUrl } from '$lib/api/adapters/config' import { getGenericPlaceholder } from './images' /** * Gets the base path for images * Returns AWS S3/CDN URL if configured, otherwise local /images path */ function getBasePath(): string { const remoteUrl = getImageBaseUrl() return remoteUrl || '/images' } /** * Gender options for job portraits */ export enum Gender { Gran = 0, // Male protagonist (a) Djeeta = 1 // Female protagonist (b) } /** * Generate job portrait URL for protagonist slot (CharacterRep/CharacterUnit) * These are smaller portrait images stored in /static/images/job-portraits/ */ export function getJobPortraitUrl(job: Job | undefined, gender: Gender = Gender.Gran): string { if (!job) { return getGenericPlaceholder() } // Convert job name to slug format (lowercase, spaces to hyphens) const slug = job.name.en.toLowerCase().replace(/\s+/g, '-') const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a' return `${getBasePath()}/job-portraits/${slug}_${genderSuffix}.png` } /** * Generate full job image URL for JobSection component * These are full job images stored in /static/images/jobs/ */ export function getJobFullImageUrl(job: Job | undefined, gender: Gender = Gender.Gran): string { if (!job) { return getGenericPlaceholder() } const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a' return `${getBasePath()}/job-zoom/${job.granblueId}_${genderSuffix}.png` } /** * Generate job icon URL * Job icons are small square icons representing the job */ export function getJobIconUrl(granblueId: string | undefined): string { if (!granblueId) { return getGenericPlaceholder() } return `${getBasePath()}/job-icons/${granblueId}.png` } /** * Generate job wide banner image URL for JobItem component * These are wider banner-style images stored in /static/images/job-wide/ */ export function getJobWideImageUrl(job: Job | undefined, gender: Gender = Gender.Gran): string { if (!job) { return getGenericPlaceholder() } const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a' return `${getBasePath()}/job-wide/${job.granblueId}_${genderSuffix}.jpg` } /** * Get job tier display name * Converts internal row codes to user-friendly names */ export function getJobTierName(row: string | number): string { const tierNames: Record = { '1': 'Class I', '2': 'Class II', '3': 'Class III', '4': 'Class IV', '5': 'Class V', ex: 'EX', ex1: 'EX', ex2: 'EXII', o1: 'Origin I' } const rowStr = row.toString().toLowerCase() return tierNames[rowStr] || `Class ${row}` } /** * Get job tier order for sorting * Returns a numeric value for sorting tiers */ export function getJobTierOrder(row: string | number): number { const tierOrder: Record = { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, ex: 6, ex1: 6, ex2: 7, o1: 8 } const rowStr = row.toString().toLowerCase() return tierOrder[rowStr] || 99 } /** * Check if a job supports accessories */ export function jobSupportsAccessories(job: Job | undefined): boolean { return job?.accessory === true } /** * Get the number of skill slots for a job * Row 1 jobs have 3 slots, all others have 4 */ export function getJobSkillSlotCount(job: Job | undefined): number { if (!job) return 0 return job.row === 1 ? 3 : 4 } /** * Check if a skill slot is available for a job */ export function isSkillSlotAvailable(job: Job | undefined, slot: number): boolean { if (!job) return false const slotCount = getJobSkillSlotCount(job) return slot >= 0 && slot < slotCount } /** * Check if a skill slot is locked (cannot be changed) * Slot 0 is locked when it contains a main skill */ export function isSkillSlotLocked( slot: number, job: Job | undefined, jobSkills: JobSkillList | undefined ): boolean { // Slot 0 is locked if it contains a main skill return slot === 0 && jobSkills?.['0']?.main === true } /** * Get skill category display name */ export function getSkillCategoryName(skill: JobSkill): string { if (skill.main) return 'Main' if (skill.sub) return 'Subskill' if (skill.emp) return 'EMP' if (skill.base) return 'Base' return 'Unknown' } /** * Get skill category color * Returns CSS color variable name */ export function getSkillCategoryColor(skill: JobSkill): string { if (skill.main) return 'var(--skill-main, #ff6b6b)' if (skill.sub) return 'var(--skill-sub, #4ecdc4)' if (skill.emp) return 'var(--skill-emp, #45b7d1)' if (skill.base) return 'var(--skill-base, #96ceb4)' return 'var(--skill-default, #888)' } /** * Format job proficiency for display * Converts proficiency numbers to weapon type names */ export function formatJobProficiency(proficiency: [number, number]): string[] { const weaponTypes: Record = { 1: 'Sabre', 2: 'Dagger', 3: 'Axe', 4: 'Spear', 5: 'Bow', 6: 'Staff', 7: 'Melee', 8: 'Harp', 9: 'Gun', 10: 'Katana' } const result: string[] = [] const type1 = proficiency[0] ? weaponTypes[proficiency[0]] : undefined if (type1) { result.push(type1) } const type2 = proficiency[1] ? weaponTypes[proficiency[1]] : undefined if (type2) { result.push(type2) } return result } /** * Check if a job is an advanced job (Row IV, V, or Extra II) */ export function isAdvancedJob(job: Job): boolean { const row = job.row.toString().toLowerCase() return row === '4' || row === '5' || row === 'ex2' } /** * Count skills by type in current skill list */ export function countSkillsByType(skills: JobSkillList): { main: number sub: number emp: number base: number } { const counts = { main: 0, sub: 0, emp: 0, base: 0 } for (let i = 0; i < 4; i++) { const skill = skills[i as keyof JobSkillList] if (skill) { if (skill.main) counts.main++ else if (skill.sub) counts.sub++ else if (skill.emp) counts.emp++ else if (skill.base) counts.base++ } } return counts } /** * Validate if a skill configuration is valid for a job */ export function validateSkillConfiguration( job: Job, skills: JobSkillList ): { valid: boolean; errors: string[] } { const errors: string[] = [] const counts = countSkillsByType(skills) // Check for advanced job constraints if (isAdvancedJob(job)) { if (counts.sub > 2) { errors.push('Maximum 2 subskills allowed for advanced jobs') } if (counts.emp > 2) { errors.push('Maximum 2 EMP skills allowed for advanced jobs') } } // Check for Row 1 constraint if (job.row === 1 && skills[3]) { errors.push('Row I jobs only support 3 skill slots') } // Check for duplicate skills const skillIds = new Set() for (let i = 0; i < 4; i++) { const skill = skills[i as keyof JobSkillList] if (skill) { if (skillIds.has(skill.id)) { errors.push(`Duplicate skill: ${skill.name.en}`) } skillIds.add(skill.id) } } return { valid: errors.length === 0, errors } }