forked from noxious/server
186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
import fs from 'fs'
|
|
import { BaseEvent } from '@/application/base/baseEvent'
|
|
import { SocketEvent } from '@/application/enums'
|
|
import type { UUID } from '@/application/types'
|
|
import { SpriteAction } from '@/entities/spriteAction'
|
|
import SpriteRepository from '@/repositories/spriteRepository'
|
|
import sharp from 'sharp'
|
|
|
|
interface SpriteImage {
|
|
url: string
|
|
offset: {
|
|
x: number
|
|
y: number
|
|
}
|
|
}
|
|
|
|
interface SpriteActionData {
|
|
action: string
|
|
sprites: SpriteImage[]
|
|
originX: number
|
|
originY: number
|
|
frameRate: number
|
|
}
|
|
|
|
interface UpdateSpritePayload {
|
|
id: UUID
|
|
name: string
|
|
spriteActions: SpriteActionData[]
|
|
}
|
|
|
|
export default class SpriteUpdateEvent extends BaseEvent {
|
|
private readonly spriteRepository: SpriteRepository = new SpriteRepository()
|
|
|
|
public listen(): void {
|
|
this.socket.on(SocketEvent.GM_SPRITE_UPDATE, this.handleEvent.bind(this))
|
|
}
|
|
|
|
private async handleEvent(data: UpdateSpritePayload, callback: (success: boolean) => void): Promise<void> {
|
|
try {
|
|
// Validate request and permissions
|
|
if (!(await this.isCharacterGM())) {
|
|
callback(false)
|
|
return
|
|
}
|
|
|
|
// Get and validate sprite
|
|
const sprite = await this.spriteRepository.getById(data.id)
|
|
if (!sprite) {
|
|
callback(false)
|
|
return
|
|
}
|
|
await this.spriteRepository.getEntityManager().populate(sprite, ['spriteActions'])
|
|
|
|
// Update sprite name
|
|
await sprite.setName(data.name).setUpdatedAt(new Date()).save()
|
|
|
|
// Process all sprite actions
|
|
const success = await this.processAllSpriteActions(data.spriteActions, sprite)
|
|
callback(success)
|
|
} catch (error) {
|
|
console.error(`Error updating sprite ${data.id}:`, error)
|
|
callback(false)
|
|
}
|
|
}
|
|
|
|
private async processAllSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise<boolean> {
|
|
try {
|
|
// Remove existing actions
|
|
const existingActions = sprite.getSpriteActions()
|
|
for (const existingAction of existingActions) {
|
|
await this.spriteRepository.getEntityManager().removeAndFlush(existingAction)
|
|
}
|
|
|
|
// Process each action
|
|
for (const actionData of actionsData) {
|
|
if (actionData.sprites.length === 0) continue
|
|
|
|
// Generate and save the sprite sheet
|
|
const frameSize = await this.generateAndSaveSpriteSheet(
|
|
actionData.sprites,
|
|
sprite.getId(),
|
|
actionData.action
|
|
)
|
|
if (!frameSize) return false
|
|
|
|
// Create and save sprite action
|
|
const spriteAction = new SpriteAction()
|
|
spriteAction.setSprite(sprite)
|
|
sprite.getSpriteActions().add(spriteAction)
|
|
|
|
spriteAction
|
|
.setAction(actionData.action)
|
|
.setSprites(actionData.sprites)
|
|
.setOriginX(actionData.originX)
|
|
.setOriginY(actionData.originY)
|
|
.setFrameWidth(frameSize.width)
|
|
.setFrameHeight(frameSize.height)
|
|
.setFrameRate(actionData.frameRate)
|
|
|
|
await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction)
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
console.error('Error processing sprite actions:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
private async generateAndSaveSpriteSheet(
|
|
sprites: SpriteImage[],
|
|
spriteId: string,
|
|
action: string
|
|
): Promise<{ width: number, height: number } | null> {
|
|
try {
|
|
if (sprites.length === 0) return { width: 0, height: 0 }
|
|
|
|
// Extract image data from sprites
|
|
const imageBuffers = await Promise.all(
|
|
sprites.map(sprite => {
|
|
const base64Data = sprite.url.split(';base64,').pop()
|
|
if (!base64Data) throw new Error('Invalid base64 image')
|
|
return Buffer.from(base64Data, 'base64')
|
|
})
|
|
)
|
|
|
|
// Get metadata for all images to find the maximum dimensions
|
|
const metadataList = await Promise.all(
|
|
imageBuffers.map(buffer => sharp(buffer).metadata())
|
|
)
|
|
|
|
// Calculate the maximum width and height across all frames
|
|
const maxWidth = Math.max(...metadataList.map(meta => meta.width || 0))
|
|
const maxHeight = Math.max(...metadataList.map(meta => meta.height || 0))
|
|
|
|
// Skip creation if we couldn't determine dimensions
|
|
if (maxWidth === 0 || maxHeight === 0) {
|
|
console.error('Could not determine sprite dimensions')
|
|
return null
|
|
}
|
|
|
|
// Resize all frames to the same dimensions
|
|
const resizedFrames = await Promise.all(
|
|
imageBuffers.map(async (buffer) => {
|
|
return sharp(buffer)
|
|
.resize({
|
|
width: maxWidth,
|
|
height: maxHeight,
|
|
fit: 'contain',
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
})
|
|
.toBuffer()
|
|
})
|
|
)
|
|
|
|
// Create sprite sheet with uniformly sized frames
|
|
const spriteSheet = await sharp({
|
|
create: {
|
|
width: maxWidth * resizedFrames.length,
|
|
height: maxHeight,
|
|
channels: 4,
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
}
|
|
})
|
|
.composite(
|
|
resizedFrames.map((buffer, index) => ({
|
|
input: buffer,
|
|
left: index * maxWidth,
|
|
top: 0
|
|
}))
|
|
)
|
|
.png()
|
|
.toBuffer()
|
|
|
|
// Save sprite sheet
|
|
const dir = `public/sprites/${spriteId}`
|
|
await fs.promises.mkdir(dir, { recursive: true })
|
|
await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet)
|
|
|
|
return { width: maxWidth, height: maxHeight }
|
|
} catch (error) {
|
|
console.error('Error generating sprite sheet:', error)
|
|
return null
|
|
}
|
|
}
|
|
} |