import fs from 'fs' import { BaseController } from '@/application/base/baseController' import Storage from '@/application/storage' import type { UUID } from '@/application/types' import CharacterHairRepository from '@/repositories/characterHairRepository' import CharacterRepository from '@/repositories/characterRepository' import CharacterTypeRepository from '@/repositories/characterTypeRepository' import type { Request, Response } from 'express' import sharp from 'sharp' interface AvatarOptions { characterTypeId: UUID characterHairId?: UUID } export class AvatarController extends BaseController { private readonly characterTypeRepository = new CharacterTypeRepository() private readonly characterHairRepository = new CharacterHairRepository() private readonly characterRepository = new CharacterRepository() /** * Get avatar by character * @param req * @param res */ public async getByName(req: Request, res: Response) { const character = await this.characterRepository.getByName(req.params.characterName!) if (!character?.characterType) { return this.sendError(res, 'Character or character type not found', 404) } return this.generateAvatar(res, { characterTypeId: character.characterType.id, characterHairId: character.characterHair?.id }) } /** * Get avatar by character type and hair * @param req * @param res */ public async getByParams(req: Request, res: Response) { return this.generateAvatar(res, { characterTypeId: req.params.characterTypeId as UUID, characterHairId: req.params.characterHairId ? (req.params.characterHairId as UUID) : undefined }) } /** * Generate avatar * @param res * @param options * @private */ private async generateAvatar(res: Response, options: AvatarOptions) { try { const characterType = await this.characterTypeRepository.getById(options.characterTypeId) if (!characterType?.sprite?.id) { return this.sendError(res, 'Character type not found', 404) } const bodySpritePath = Storage.getPublicPath('sprites', characterType.sprite.id, 'idle_right_down.png') if (!fs.existsSync(bodySpritePath)) { return this.sendError(res, 'Body sprite file not found', 404) } // Get body sprite metadata const bodyMetadata = await sharp(bodySpritePath).metadata() let avatar = sharp(bodySpritePath).extend({ top: 0, bottom: 0, background: { r: 0, g: 0, b: 0, alpha: 0 } }) if (options.characterHairId) { const characterHair = await this.characterHairRepository.getById(options.characterHairId) if (characterHair?.sprite?.id) { const hairSpritePath = Storage.getPublicPath('sprites', characterHair.sprite.id, 'front.png') if (fs.existsSync(hairSpritePath)) { // Resize hair sprite to match body dimensions const resizedHair = await sharp(hairSpritePath) .resize(bodyMetadata.width, bodyMetadata.height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }) .toBuffer() avatar = avatar.composite([ { input: resizedHair, left: 0, top: -27 // Apply vertical offset } ]) } } } res.setHeader('Content-Type', 'image/png') return avatar.pipe(res) } catch (error) { console.error('Avatar generation error:', error) return this.sendError(res, 'Error generating avatar', 500) } } }