Finish sprite gen. update
This commit is contained in:
parent
c59b391a6a
commit
5b06386a39
Binary file not shown.
@ -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([
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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}`
|
||||||
|
@ -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": {
|
||||||
|
@ -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\`);`);
|
Loading…
x
Reference in New Issue
Block a user