1
0
forked from noxious/server

Finish sprite gen. update

This commit is contained in:
Dennis Postma 2025-02-21 02:01:51 +01:00
parent c59b391a6a
commit 5b06386a39
6 changed files with 87 additions and 64 deletions

Binary file not shown.

View File

@ -81,11 +81,11 @@ export class AvatarController extends BaseController {
if (fs.existsSync(hairSpritePath)) { if (fs.existsSync(hairSpritePath)) {
// Resize hair sprite to match body dimensions // Resize hair sprite to match body dimensions
const resizedHair = await sharp(hairSpritePath) const resizedHair = await sharp(hairSpritePath)
.resize(bodyMetadata.width, bodyMetadata.height, { .resize(bodyMetadata.width, bodyMetadata.height, {
fit: 'contain', fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
}) })
.toBuffer() .toBuffer()
avatar = avatar.composite([ avatar = avatar.composite([
{ {

View File

@ -14,11 +14,11 @@ export class BaseSprite extends BaseEntity {
@OneToMany({ mappedBy: 'sprite', orphanRemoval: true }) @OneToMany({ mappedBy: 'sprite', orphanRemoval: true })
spriteActions = new Collection<SpriteAction>(this) spriteActions = new Collection<SpriteAction>(this)
@Property() @Property({ nullable: true })
width: number = 0 width: number | null = null
@Property() @Property({ nullable: true })
height: number = 0 height: number | null = null
@Property() @Property()
createdAt = new Date() createdAt = new Date()
@ -53,7 +53,7 @@ export class BaseSprite extends BaseEntity {
return this.spriteActions return this.spriteActions
} }
setWidth(width: number) { setWidth(width: number | null) {
this.width = width this.width = width
return this return this
} }
@ -62,7 +62,7 @@ export class BaseSprite extends BaseEntity {
return this.width return this.width
} }
setHeight(height: number) { setHeight(height: number | null) {
this.height = height this.height = height
return this return this
} }

View File

@ -31,8 +31,8 @@ interface EffectiveDimensions {
type Payload = { type Payload = {
id: UUID id: UUID
name: string name: string
width: number width: number | null
height: number height: number | null
spriteActions: Array<{ spriteActions: Array<{
action: string action: string
sprites: SpriteImage[] sprites: SpriteImage[]
@ -57,12 +57,17 @@ export default class SpriteUpdateEvent extends BaseEvent {
await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) await spriteRepository.getEntityManager().populate(sprite, ['spriteActions'])
// Update sprite in database // Update sprite in database with width/height if provided
await sprite.setName(data.name).setWidth(data.width).setHeight(data.height).setUpdatedAt(new Date()).save() await sprite
.setName(data.name)
.setWidth(data.width ?? sprite.getWidth())
.setHeight(data.height ?? sprite.getHeight())
.setUpdatedAt(new Date())
.save()
// First verify all sprite sheets can be generated // First verify all sprite sheets can be generated
for (const actionData of data.spriteActions) { for (const actionData of data.spriteActions) {
if (!(await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action))) { if (!(await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action, data.width ?? 0, data.height ?? 0))) {
return callback(false) return callback(false)
} }
} }
@ -80,24 +85,25 @@ export default class SpriteUpdateEvent extends BaseEvent {
const imageData = await Promise.all(actionData.sprites.map((sprite) => this.processImage(sprite))) const imageData = await Promise.all(actionData.sprites.map((sprite) => this.processImage(sprite)))
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
// Calculate total height needed for the sprite sheet // Calculate maximum dimensions
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height)) const maxWidth = data.width ?? Math.max(...effectiveDimensions.map((d) => d.width))
const maxHeight = data.height ?? Math.max(...effectiveDimensions.map((d) => d.height))
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) const maxTop = Math.max(...effectiveDimensions.map((d) => d.top))
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom))
const totalHeight = maxHeight + maxTop + maxBottom const totalHeight = data.height ?? maxHeight + maxTop + maxBottom
const spriteAction = new SpriteAction() const spriteAction = new SpriteAction()
spriteAction.setSprite(sprite) spriteAction.setSprite(sprite)
sprite.getSpriteActions().add(spriteAction) sprite.getSpriteActions().add(spriteAction)
spriteAction spriteAction
.setAction(actionData.action) .setAction(actionData.action)
.setSprites(actionData.sprites) .setSprites(actionData.sprites)
.setOriginX(actionData.originX) .setOriginX(actionData.originX)
.setOriginY(actionData.originY) .setOriginY(actionData.originY)
.setFrameWidth(await this.calculateMaxWidth(actionData.sprites)) .setFrameWidth(maxWidth)
.setFrameHeight(totalHeight) .setFrameHeight(totalHeight)
.setFrameRate(actionData.frameRate) .setFrameRate(actionData.frameRate)
await spriteRepository.getEntityManager().persistAndFlush(spriteAction) await spriteRepository.getEntityManager().persistAndFlush(spriteAction)
} }
@ -109,7 +115,7 @@ export default class SpriteUpdateEvent extends BaseEvent {
} }
} }
private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<boolean> { private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string, containerWidth: number, containerHeight: number): Promise<boolean> {
try { try {
if (!sprites.length) return true if (!sprites.length) return true
@ -118,37 +124,56 @@ export default class SpriteUpdateEvent extends BaseEvent {
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
// Calculate maximum dimensions // Calculate maximum dimensions
const maxWidth = Math.max(...effectiveDimensions.map((d) => d.width)) const maxWidth = containerWidth > 0 ? containerWidth : Math.max(...effectiveDimensions.map((d) => d.width))
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height)) const maxHeight = containerHeight > 0 ? containerHeight : Math.max(...effectiveDimensions.map((d) => d.height))
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) const maxTop = Math.max(...effectiveDimensions.map((d) => d.top))
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom))
// Calculate total height needed // Calculate total height needed
const totalHeight = maxHeight + maxTop + maxBottom const totalHeight = containerHeight > 0 ? containerHeight : maxHeight + maxTop + maxBottom
// Process images and create sprite sheet // Process images and create sprite sheet
const processedImages = await Promise.all( const processedImages = await Promise.all(
sprites.map(async (sprite, index) => { sprites.map(async (sprite) => {
const { width, height, offsetX, offsetY } = await this.processImage(sprite) const { width, height, offsetX, offsetY } = await this.processImage(sprite)
const uri = sprite.url.split(';base64,').pop() const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image') if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64') const buffer = Buffer.from(uri, 'base64')
// Create individual frame // Calculate position based on container or offset
const left = offsetX >= 0 ? offsetX : 0 // If container dimensions are set, position at top center
const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0) const left = containerWidth > 0 ? Math.floor((maxWidth - width) / 2) : offsetX >= 0 ? offsetX : 0
return sharp({
create: { const top =
width: maxWidth, containerHeight > 0
height: totalHeight, ? 0 // Place at top when container dimensions are set
channels: 4, : totalHeight - height - (offsetY >= 0 ? offsetY : 0)
background: { r: 0, g: 0, b: 0, alpha: 0 }
} return sharp({
}) create: {
.composite([{ input: buffer, left, top: verticalOffset }]) width: maxWidth,
.png() height: totalHeight,
.toBuffer() channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
}) })
.composite([
{
input: buffer,
left,
top,
...(containerWidth > 0 || containerHeight > 0
? {
width: containerWidth || undefined,
height: containerHeight || undefined,
fit: 'contain'
}
: {})
}
])
.png()
.toBuffer()
})
) )
// Combine frames into sprite sheet // Combine frames into sprite sheet
@ -160,15 +185,15 @@ export default class SpriteUpdateEvent extends BaseEvent {
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
} }
}) })
.composite( .composite(
processedImages.map((buffer, index) => ({ processedImages.map((buffer, index) => ({
input: buffer, input: buffer,
left: index * maxWidth, left: index * maxWidth,
top: 0 top: 0
})) }))
) )
.png() .png()
.toBuffer() .toBuffer()
// Ensure directory exists // Ensure directory exists
const dir = `public/sprites/${spriteId}` const dir = `public/sprites/${spriteId}`

View File

@ -745,9 +745,8 @@
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": true,
"length": null, "length": null,
"default": "0",
"mappedType": "integer" "mappedType": "integer"
}, },
"height": { "height": {
@ -756,9 +755,8 @@
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": true,
"length": null, "length": null,
"default": "0",
"mappedType": "integer" "mappedType": "integer"
}, },
"created_at": { "created_at": {

View File

@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'; import { Migration } from '@mikro-orm/migrations';
export class Migration20250219234315 extends Migration { export class Migration20250221004940 extends Migration {
override async up(): Promise<void> { override async up(): Promise<void> {
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null default '', \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json not null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null default '', \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json not null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
@ -22,7 +22,7 @@ export class Migration20250219234315 extends Migration {
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`); this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`); this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 0, \`height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int null, \`height\` int null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`); this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`);