diff --git a/public/assets.zip b/public/assets.zip index 7ff57db..bfbd777 100644 Binary files a/public/assets.zip and b/public/assets.zip differ diff --git a/src/controllers/avatar.ts b/src/controllers/avatar.ts index a4e5984..7256a47 100644 --- a/src/controllers/avatar.ts +++ b/src/controllers/avatar.ts @@ -81,11 +81,11 @@ export class AvatarController extends BaseController { 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() + .resize(bodyMetadata.width, bodyMetadata.height, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 } + }) + .toBuffer() avatar = avatar.composite([ { diff --git a/src/entities/base/sprite.ts b/src/entities/base/sprite.ts index f490d20..874690a 100644 --- a/src/entities/base/sprite.ts +++ b/src/entities/base/sprite.ts @@ -14,11 +14,11 @@ export class BaseSprite extends BaseEntity { @OneToMany({ mappedBy: 'sprite', orphanRemoval: true }) spriteActions = new Collection(this) - @Property() - width: number = 0 + @Property({ nullable: true }) + width: number | null = null - @Property() - height: number = 0 + @Property({ nullable: true }) + height: number | null = null @Property() createdAt = new Date() @@ -53,7 +53,7 @@ export class BaseSprite extends BaseEntity { return this.spriteActions } - setWidth(width: number) { + setWidth(width: number | null) { this.width = width return this } @@ -62,7 +62,7 @@ export class BaseSprite extends BaseEntity { return this.width } - setHeight(height: number) { + setHeight(height: number | null) { this.height = height return this } diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 91a3936..b87d2aa 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -31,8 +31,8 @@ interface EffectiveDimensions { type Payload = { id: UUID name: string - width: number - height: number + width: number | null + height: number | null spriteActions: Array<{ action: string sprites: SpriteImage[] @@ -57,12 +57,17 @@ export default class SpriteUpdateEvent extends BaseEvent { await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) - // Update sprite in database - await sprite.setName(data.name).setWidth(data.width).setHeight(data.height).setUpdatedAt(new Date()).save() + // Update sprite in database with width/height if provided + 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 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) } } @@ -80,24 +85,25 @@ export default class SpriteUpdateEvent extends BaseEvent { 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)) + // Calculate maximum dimensions + 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 maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) - const totalHeight = maxHeight + maxTop + maxBottom + const totalHeight = data.height ?? maxHeight + maxTop + maxBottom 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(await this.calculateMaxWidth(actionData.sprites)) - .setFrameHeight(totalHeight) - .setFrameRate(actionData.frameRate) + .setAction(actionData.action) + .setSprites(actionData.sprites) + .setOriginX(actionData.originX) + .setOriginY(actionData.originY) + .setFrameWidth(maxWidth) + .setFrameHeight(totalHeight) + .setFrameRate(actionData.frameRate) 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 { + private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string, containerWidth: number, containerHeight: number): Promise { try { if (!sprites.length) return true @@ -118,37 +124,56 @@ export default class SpriteUpdateEvent extends BaseEvent { const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate maximum dimensions - const maxWidth = Math.max(...effectiveDimensions.map((d) => d.width)) - const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height)) + const maxWidth = containerWidth > 0 ? containerWidth : Math.max(...effectiveDimensions.map((d) => d.width)) + const maxHeight = containerHeight > 0 ? containerHeight : Math.max(...effectiveDimensions.map((d) => d.height)) const maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) // Calculate total height needed - const totalHeight = maxHeight + maxTop + maxBottom + const totalHeight = containerHeight > 0 ? containerHeight : maxHeight + maxTop + maxBottom // Process images and create sprite sheet const processedImages = await Promise.all( - sprites.map(async (sprite, index) => { - const { width, height, offsetX, offsetY } = await this.processImage(sprite) - const uri = sprite.url.split(';base64,').pop() - if (!uri) throw new Error('Invalid base64 image') - const buffer = Buffer.from(uri, 'base64') + sprites.map(async (sprite) => { + const { width, height, offsetX, offsetY } = await this.processImage(sprite) + const uri = sprite.url.split(';base64,').pop() + if (!uri) throw new Error('Invalid base64 image') + const buffer = Buffer.from(uri, 'base64') - // 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() + // Calculate position based on container or offset + // If container dimensions are set, position at top center + const left = containerWidth > 0 ? Math.floor((maxWidth - width) / 2) : offsetX >= 0 ? offsetX : 0 + + const top = + containerHeight > 0 + ? 0 // Place at top when container dimensions are set + : 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, + ...(containerWidth > 0 || containerHeight > 0 + ? { + width: containerWidth || undefined, + height: containerHeight || undefined, + fit: 'contain' + } + : {}) + } + ]) + .png() + .toBuffer() + }) ) // Combine frames into sprite sheet @@ -160,15 +185,15 @@ export default class SpriteUpdateEvent extends BaseEvent { background: { r: 0, g: 0, b: 0, alpha: 0 } } }) - .composite( - processedImages.map((buffer, index) => ({ - input: buffer, - left: index * maxWidth, - top: 0 - })) - ) - .png() - .toBuffer() + .composite( + processedImages.map((buffer, index) => ({ + input: buffer, + left: index * maxWidth, + top: 0 + })) + ) + .png() + .toBuffer() // Ensure directory exists const dir = `public/sprites/${spriteId}` diff --git a/src/migrations/.snapshot-game.json b/src/migrations/.snapshot-game.json index 4c2a3c9..77e5b6e 100644 --- a/src/migrations/.snapshot-game.json +++ b/src/migrations/.snapshot-game.json @@ -745,9 +745,8 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, + "nullable": true, "length": null, - "default": "0", "mappedType": "integer" }, "height": { @@ -756,9 +755,8 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, + "nullable": true, "length": null, - "default": "0", "mappedType": "integer" }, "created_at": { diff --git a/src/migrations/Migration20250219234315.ts b/src/migrations/Migration20250221004940.ts similarity index 98% rename from src/migrations/Migration20250219234315.ts rename to src/migrations/Migration20250221004940.ts index 6a787fe..9b33416 100644 --- a/src/migrations/Migration20250219234315.ts +++ b/src/migrations/Migration20250221004940.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250219234315 extends Migration { +export class Migration20250221004940 extends Migration { override async up(): Promise { 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_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(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`);