diff --git a/src/application/base/baseEntity.ts b/src/application/base/baseEntity.ts index 65fee38..570859e 100644 --- a/src/application/base/baseEntity.ts +++ b/src/application/base/baseEntity.ts @@ -46,7 +46,10 @@ export abstract class BaseEntity { throw error } } catch (error) { - this.logger.error(`Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`) + const errorMessage = error instanceof Error ? error.message : + error && typeof error === 'object' && 'toString' in error ? error.toString() : + String(error) + this.logger.error(`Failed to ${actionDescription}: ${errorMessage}`) throw error } } diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index db9af24..6cf75e1 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -1,61 +1,17 @@ -import { writeFile, mkdir } from 'node:fs/promises' - -import sharp from 'sharp' - import { BaseEvent } from '#application/base/baseEvent' -import Storage from '#application/storage' -import { SpriteAction } from '#entities/spriteAction' +import { UUID } from '#application/types' import SpriteRepository from '#repositories/spriteRepository' -// Constants -const ISOMETRIC_CONFIG = { - tileWidth: 64, - tileHeight: 32, - centerOffset: 32, - bodyRatios: { - topStart: 0.15, - topEnd: 0.45, - weightUpper: 0.7, - weightLower: 0.3 - } -} as const - -// Types -interface ContentBounds { - left: number - right: number - top: number - bottom: number - width: number - height: number -} - -interface SpriteActionInput extends Omit { - sprites: string[] -} - -interface UpdatePayload { - id: string +type Payload = { + id: UUID name: string - spriteActions: SpriteAction[] -} - -interface ProcessedSpriteAction extends SpriteActionInput { - frameWidth: number - frameHeight: number - buffersWithDimensions: ProcessedFrame[] -} - -interface ProcessedFrame { - buffer: Buffer - width: number - height: number -} - -interface SpriteAnalysis { - massCenter: number - spinePosition: number - contentBounds: ContentBounds + spriteActions: Array<{ + action: string + sprites: string[] + originX: number + originY: number + frameRate: number + }> } export default class SpriteUpdateEvent extends BaseEvent { @@ -63,324 +19,26 @@ export default class SpriteUpdateEvent extends BaseEvent { this.socket.on('gm:sprite:update', this.handleEvent.bind(this)) } - private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise { + private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise { try { if (!(await this.isCharacterGM())) return - const parsedActions = this.validateSpriteActions(payload.spriteActions) + const spriteRepository = new SpriteRepository() + const sprite = await spriteRepository.getById(data.id) + if (!sprite) return callback(false) - // Process sprites - const processedActions = await Promise.all( - parsedActions.map(async (action) => { - const spriteBuffers = await this.convertBase64ToBuffers(action.sprites) - const frameWidth = ISOMETRIC_CONFIG.tileWidth - const frameHeight = await this.calculateOptimalHeight(spriteBuffers) - const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight) + await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']); - return { - ...action, - frameWidth, - frameHeight, - buffersWithDimensions: processedFrames - } - }) - ) + // Update sprite in database + await sprite + .setName(data.name) + .setSpriteActions(data.spriteActions) + .save() - await Promise.all([ - this.updateDatabase(payload.id, payload.name, processedActions), - this.saveSpritesToDisk( - payload.id, - processedActions.filter((a) => a.buffersWithDimensions.length > 0) - ) - ]) - - callback(true) + return callback(true) } catch (error) { - this.handleError(error, payload.id, callback) + console.error(`Error updating sprite ${data.id}:`, error) + return callback(false) } } - - private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] { - try { - const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[] - if (!Array.isArray(parsed)) { - throw new Error('Sprite actions must be an array') - } - return parsed - } catch (error) { - throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`) - } - } - - private async convertBase64ToBuffers(sprites: string[]): Promise { - return sprites.map((sprite) => Buffer.from(sprite.split(',')[1], 'base64')) - } - - private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise { - return Promise.all( - buffers.map(async (buffer) => { - const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight) - return { - buffer: normalizedBuffer, - width: frameWidth, - height: frameHeight - } - }) - ) - } - - private async calculateOptimalHeight(buffers: Buffer[]): Promise { - if (!buffers.length) return ISOMETRIC_CONFIG.tileHeight // Return default height if no buffers - - const heights = await Promise.all( - buffers.map(async (buffer) => { - const bounds = await this.findContentBounds(buffer) - return bounds.height - }) - ) - return Math.ceil(Math.max(...heights) / 2) * 2 - } - - private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise { - const analysis = await this.analyzeIsometricSprite(buffer) - const idealCenter = Math.floor(frameWidth / 2) - const offset = Math.round(idealCenter - analysis.massCenter) - - // Process the input sprite - const processedInput = await sharp(buffer) - .ensureAlpha() - .resize({ - width: frameWidth, // Set maximum width - height: frameHeight, // Set maximum height - fit: 'inside', // Ensure image fits within dimensions - kernel: sharp.kernel.nearest, - position: 'center', - withoutEnlargement: true // Don't enlarge smaller images - }) - .png({ - compressionLevel: 9, - adaptiveFiltering: false, - palette: true, - quality: 100, - colors: 256 - }) - .toBuffer() - - // Create the final composition - return sharp({ - create: { - width: frameWidth, - height: frameHeight, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 0 } - } - }) - .composite([ - { - input: processedInput, - left: offset, - top: 0, - blend: 'over' - } - ]) - .png({ - compressionLevel: 9, - adaptiveFiltering: false, - palette: true, - quality: 100, - colors: 256 - }) - .toBuffer() - } - - private async analyzeIsometricSprite(buffer: Buffer): Promise { - const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true }) - const { width, height } = info - const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart) - const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd) - - const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd) - const spinePosition = this.findSpinePosition(upperBodyDensity) - const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity) - - return { - massCenter, - spinePosition, - contentBounds: bounds - } - } - - private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) { - const columnDensity = new Array(width).fill(0) - const upperBodyDensity = new Array(width).fill(0) - const bounds = { left: width, right: 0, top: height, bottom: 0 } - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - if (data[(y * width + x) * 4 + 3] > 0) { - columnDensity[x]++ - if (y >= upperStart && y <= upperEnd) { - upperBodyDensity[x]++ - } - this.updateBounds(bounds, x, y) - } - } - } - - return { - columnDensity, - upperBodyDensity, - bounds: { - ...bounds, - width: bounds.right - bounds.left + 1, - height: bounds.bottom - bounds.top + 1 - } - } - } - - private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void { - bounds.left = Math.min(bounds.left, x) - bounds.right = Math.max(bounds.right, x) - bounds.top = Math.min(bounds.top, y) - bounds.bottom = Math.max(bounds.bottom, y) - } - - private findSpinePosition(density: number[]): number { - return density.reduce((maxIdx, curr, idx, arr) => (curr > arr[maxIdx] ? idx : maxIdx), 0) - } - - private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number { - const upperMassCenter = this.calculateMassCenter(upperBodyDensity) - const lowerMassCenter = this.calculateMassCenter(columnDensity) - - return Math.round(upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper + lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower) - } - - private calculateMassCenter(density: number[]): number { - const totalMass = density.reduce((sum, mass) => sum + mass, 0) - if (!totalMass) return 0 - - const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0) - return Math.round(weightedSum / totalMass) - } - - private async findContentBounds(buffer: Buffer) { - const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true }) - - const width = info.width - const height = info.height - - let left = width - let right = 0 - let top = height - let bottom = 0 - - // Find actual content boundaries by checking alpha channel - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = (y * width + x) * 4 - if (data[idx + 3] > 0) { - // If pixel is not transparent - left = Math.min(left, x) - right = Math.max(right, x) - top = Math.min(top, y) - bottom = Math.max(bottom, y) - } - } - } - - return { - width: right - left + 1, - height: bottom - top + 1, - leftOffset: left, - topOffset: top - } - } - - private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise { - const publicFolder = Storage.getPublicPath('sprites', id) - await mkdir(publicFolder, { recursive: true }) - - await Promise.all( - actions.map(async (action) => { - const spritesheet = await this.createSpritesheet(action.buffersWithDimensions) - await writeFile(Storage.getPublicPath('sprites', id, `${action.action}.png`), spritesheet) - }) - ) - } - - private async createSpritesheet(frames: ProcessedFrame[]): Promise { - const background = await sharp({ - create: { - width: ISOMETRIC_CONFIG.tileWidth * frames.length, - height: frames[0].height, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 0 } - } - }) - .png({ - compressionLevel: 9, - adaptiveFiltering: false, - palette: true, - quality: 100, - colors: 256, - dither: 0 - }) - .toBuffer() - - return sharp(background) - .composite( - frames.map((frame, index) => ({ - input: frame.buffer, - left: index * ISOMETRIC_CONFIG.tileWidth, - top: 0, - blend: 'over' - })) - ) - .png({ - compressionLevel: 9, - adaptiveFiltering: false, - palette: true, - quality: 100, - colors: 256, - dither: 0 - }) - .toBuffer() - } - - private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise { - await prisma.sprite.update({ - where: { id }, - data: { - name, - spriteActions: { - deleteMany: { spriteId: id }, - create: actions.map(this.mapActionToDatabase) - } - } - }) - - await (await SpriteRepository.getById(id))?.setName(name).setSpriteActions(actions).update() - } - - private mapActionToDatabase(action: ProcessedSpriteAction) { - return { - action: action.action, - sprites: action.sprites, - originX: action.originX, - originY: action.originY, - frameWidth: action.frameWidth, - frameHeight: action.frameHeight, - frameRate: action.frameRate - } - } - - private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void { - this.logger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`) - callback(false) - } - - private getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error) - } -} +} \ No newline at end of file