forked from noxious/server
sprite work
This commit is contained in:
@ -14,20 +14,6 @@ interface SpriteImage {
|
||||
}
|
||||
}
|
||||
|
||||
interface ImageDimensions {
|
||||
width: number
|
||||
height: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
interface EffectiveDimensions {
|
||||
width: number
|
||||
height: number
|
||||
top: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
interface SpriteActionData {
|
||||
action: string
|
||||
sprites: SpriteImage[]
|
||||
@ -51,207 +37,150 @@ export default class SpriteUpdateEvent extends BaseEvent {
|
||||
|
||||
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 in database
|
||||
// Update sprite name
|
||||
await sprite.setName(data.name).setUpdatedAt(new Date()).save()
|
||||
|
||||
// First verify all sprite sheets can be generated
|
||||
const allSheetsGenerated = await this.verifyAllSpriteSheets(data.spriteActions, sprite.getId())
|
||||
if (!allSheetsGenerated) {
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove existing actions
|
||||
await this.removeExistingActions(sprite)
|
||||
|
||||
// Create new actions
|
||||
await this.createNewSpriteActions(data.spriteActions, sprite)
|
||||
|
||||
callback(true)
|
||||
// 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 verifyAllSpriteSheets(actionsData: SpriteActionData[], spriteId: string): Promise<boolean> {
|
||||
for (const actionData of actionsData) {
|
||||
if (!(await this.generateSpriteSheet(actionData.sprites, spriteId, actionData.action))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private async removeExistingActions(sprite: any): Promise<void> {
|
||||
const existingActions = sprite.getSpriteActions()
|
||||
for (const existingAction of existingActions) {
|
||||
await this.spriteRepository.getEntityManager().removeAndFlush(existingAction)
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise<void> {
|
||||
for (const actionData of actionsData) {
|
||||
// Process images and calculate dimensions
|
||||
const imageData = await Promise.all(actionData.sprites.map((sprite) => this.processImage(sprite)))
|
||||
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
|
||||
|
||||
// Calculate total height needed for the sprite sheet
|
||||
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height), 0)
|
||||
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top), 0)
|
||||
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom), 0)
|
||||
const totalHeight = maxHeight + maxTop + maxBottom
|
||||
|
||||
const spriteAction = new SpriteAction()
|
||||
spriteAction.setSprite(sprite)
|
||||
sprite.getSpriteActions().add(spriteAction)
|
||||
|
||||
const maxWidth = await this.calculateMaxWidth(actionData.sprites)
|
||||
|
||||
spriteAction
|
||||
.setAction(actionData.action)
|
||||
.setSprites(actionData.sprites)
|
||||
.setOriginX(actionData.originX)
|
||||
.setOriginY(actionData.originY)
|
||||
.setFrameWidth(maxWidth)
|
||||
.setFrameHeight(totalHeight)
|
||||
.setFrameRate(actionData.frameRate)
|
||||
|
||||
await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction)
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<boolean> {
|
||||
private async processAllSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise<boolean> {
|
||||
try {
|
||||
if (!sprites.length) return true
|
||||
// Remove existing actions
|
||||
const existingActions = sprite.getSpriteActions()
|
||||
for (const existingAction of existingActions) {
|
||||
await this.spriteRepository.getEntityManager().removeAndFlush(existingAction)
|
||||
}
|
||||
|
||||
// Process all images and get their dimensions
|
||||
const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite)))
|
||||
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
|
||||
// Process each action
|
||||
for (const actionData of actionsData) {
|
||||
if (actionData.sprites.length === 0) continue
|
||||
|
||||
// Calculate maximum dimensions
|
||||
const maxWidth = Math.max(...effectiveDimensions.map((d) => d.width), 0)
|
||||
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height), 0)
|
||||
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top), 0)
|
||||
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom), 0)
|
||||
// Generate and save the sprite sheet
|
||||
const frameSize = await this.generateAndSaveSpriteSheet(
|
||||
actionData.sprites,
|
||||
sprite.getId(),
|
||||
actionData.action
|
||||
)
|
||||
if (!frameSize) return false
|
||||
|
||||
// Calculate total height needed
|
||||
const totalHeight = maxHeight + maxTop + maxBottom
|
||||
// Create and save sprite action
|
||||
const spriteAction = new SpriteAction()
|
||||
spriteAction.setSprite(sprite)
|
||||
sprite.getSpriteActions().add(spriteAction)
|
||||
|
||||
// Process images and create sprite sheet
|
||||
const processedImages = await Promise.all(
|
||||
sprites.map(async (sprite) => {
|
||||
const { width, height, offsetX, offsetY } = await this.processImage(sprite)
|
||||
const uri = this.extractBase64Data(sprite.url)
|
||||
const buffer = Buffer.from(uri, 'base64')
|
||||
spriteAction
|
||||
.setAction(actionData.action)
|
||||
.setSprites(actionData.sprites)
|
||||
.setOriginX(actionData.originX)
|
||||
.setOriginY(actionData.originY)
|
||||
.setFrameWidth(frameSize.width)
|
||||
.setFrameHeight(frameSize.height)
|
||||
.setFrameRate(actionData.frameRate)
|
||||
|
||||
// Create individual frame
|
||||
const left = offsetX >= 0 ? offsetX : 0
|
||||
const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0)
|
||||
return sharp({
|
||||
create: {
|
||||
width: maxWidth,
|
||||
height: totalHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite([{ input: buffer, left, top: verticalOffset }])
|
||||
.png()
|
||||
.toBuffer()
|
||||
})
|
||||
)
|
||||
|
||||
// Combine frames into sprite sheet
|
||||
const spriteSheet = await this.createSpriteSheetImage(processedImages, maxWidth, totalHeight)
|
||||
|
||||
// Save the sprite sheet
|
||||
await this.saveSpriteSheet(spriteId, action, spriteSheet)
|
||||
await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error generating sprite sheet:', error)
|
||||
console.error('Error processing sprite actions:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async createSpriteSheetImage(processedImages: Buffer[], maxWidth: number, totalHeight: number): Promise<Buffer> {
|
||||
return sharp({
|
||||
create: {
|
||||
width: maxWidth * processedImages.length,
|
||||
height: totalHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite(
|
||||
processedImages.map((buffer, index) => ({
|
||||
input: buffer,
|
||||
left: index * maxWidth,
|
||||
top: 0
|
||||
}))
|
||||
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')
|
||||
})
|
||||
)
|
||||
.png()
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async saveSpriteSheet(spriteId: string, action: string, spriteSheet: Buffer): Promise<void> {
|
||||
// Ensure directory exists
|
||||
const dir = `public/sprites/${spriteId}`
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
// Get metadata for all images to find the maximum dimensions
|
||||
const metadataList = await Promise.all(
|
||||
imageBuffers.map(buffer => sharp(buffer).metadata())
|
||||
)
|
||||
|
||||
// Save the sprite sheet
|
||||
await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet)
|
||||
}
|
||||
// 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))
|
||||
|
||||
private extractBase64Data(url: string): string {
|
||||
const uri = url.split(';base64,').pop()
|
||||
if (!uri) throw new Error('Invalid base64 image')
|
||||
return uri
|
||||
}
|
||||
// Skip creation if we couldn't determine dimensions
|
||||
if (maxWidth === 0 || maxHeight === 0) {
|
||||
console.error('Could not determine sprite dimensions')
|
||||
return null
|
||||
}
|
||||
|
||||
private async processImage(sprite: SpriteImage): Promise<ImageDimensions> {
|
||||
const uri = this.extractBase64Data(sprite.url)
|
||||
const buffer = Buffer.from(uri, 'base64')
|
||||
const metadata = await sharp(buffer).metadata()
|
||||
return {
|
||||
width: metadata.width ?? 0,
|
||||
height: metadata.height ?? 0,
|
||||
offsetX: sprite.offset?.x ?? 0,
|
||||
offsetY: sprite.offset?.y ?? 0
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
private calculateEffectiveDimensions(imageDimensions: ImageDimensions): EffectiveDimensions {
|
||||
return {
|
||||
width: imageDimensions.width + Math.abs(imageDimensions.offsetX),
|
||||
height: imageDimensions.height + Math.abs(imageDimensions.offsetY),
|
||||
top: imageDimensions.offsetY >= 0 ? imageDimensions.offsetY : 0,
|
||||
bottom: imageDimensions.offsetY < 0 ? Math.abs(imageDimensions.offsetY) : 0
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateMaxWidth(sprites: SpriteImage[]): Promise<number> {
|
||||
if (!sprites.length) return 0
|
||||
|
||||
// Process all images and get their dimensions
|
||||
const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite)))
|
||||
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
|
||||
|
||||
// Calculate maximum width needed
|
||||
return Math.max(...effectiveDimensions.map((d) => d.width), 0)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user