From 086c7cd6d616e119bc2daacc19dd856e82a5f6c0 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sat, 15 Feb 2025 21:35:16 +0100 Subject: [PATCH 01/43] Character move bug fix If already walking and then select an invalid position, isMoving was kept true. This is fixed. --- src/events/map/characterMove.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 633d127..b518836 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -6,6 +6,7 @@ import MapCharacter from '@/models/mapCharacter' import MapEventTileRepository from '@/repositories/mapEventTileRepository' import CharacterService from '@/services/characterMoveService' import TeleportService from '@/services/characterTeleportService' +import type {Character} from "@/entities/character"; export default class CharacterMove extends BaseEvent { private readonly characterService = CharacterService @@ -41,8 +42,6 @@ export default class CharacterMove extends BaseEvent { const movementValidation = this.characterService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { - this.logger.warn(`Suspicious movement detected: ${this.socket.characterId}`) - // Force position reset character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y) this.broadcastMovement(character, false) return @@ -71,10 +70,9 @@ export default class CharacterMove extends BaseEvent { this.lastKnownPosition = { x: currentX, y: currentY } // Calculate path to target position - const path = await this.characterService.calculatePath(mapCharacter.character, Math.floor(positionX), Math.floor(positionY)) + const path = await this.characterService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { - this.io.in(mapCharacter.character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVEERROR, 'No valid path found') return } @@ -105,6 +103,7 @@ export default class CharacterMove extends BaseEvent { try { for (let i = 0; i < path.length - 1; i++) { if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) { + this.broadcastMovement(character, false) return } @@ -118,8 +117,7 @@ export default class CharacterMove extends BaseEvent { nextTile = path[i + 1] if (!currentTile || !nextTile || !this.isValidStep(currentTile, nextTile)) { - this.logger.error('Invalid movement step detected') - break + return } // Update character rotation and position in a single operation @@ -139,7 +137,9 @@ export default class CharacterMove extends BaseEvent { } // Broadcast movement - this.broadcastMovement(character, true) + if (mapCharacter.isMoving && mapCharacter.currentPath === path) { + this.broadcastMovement(character, true) + } // Apply movement delay between steps if (i < path.length - 2) { @@ -181,7 +181,7 @@ export default class CharacterMove extends BaseEvent { } } - private broadcastMovement(character: any, isMoving: boolean): void { + private broadcastMovement(character: Character, isMoving: boolean): void { this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { characterId: character.id, positionX: character.getPositionX(), From 5acebfe3772f4c6c77ddaa0249d645fa6881ce06 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sat, 15 Feb 2025 22:27:57 +0100 Subject: [PATCH 02/43] Cleaned characterMove event, moved some of its logic into char. move service --- package-lock.json | 6 +-- src/entities/base/mapObject.ts | 4 +- .../assetManager/mapObject/update.ts | 2 +- src/events/map/characterMove.ts | 52 ++++++------------- src/services/characterMoveService.ts | 28 ++++++++-- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8283357..1ce232b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1932,9 +1932,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.0.tgz", - "integrity": "sha512-GGfKu2DHGIvbnMtQjR/82wvWsdCaGxN5JGR3pvKd1mkDI9DsWn8r0+pAzZ6Y4ImWXFaetaAqywOhv2Ik0R2m3g==", + "version": "5.41.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.1.tgz", + "integrity": "sha512-bVQGR4ARM+wRJSw66114AKwO8SwS2ZF5TIvwQ9NL6Iepq6f8jnG8EjMMXL8J1pyR1eNz5bOikPeED/8ErLU6FQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/src/entities/base/mapObject.ts b/src/entities/base/mapObject.ts index 2663acb..9f28f6f 100644 --- a/src/entities/base/mapObject.ts +++ b/src/entities/base/mapObject.ts @@ -14,7 +14,7 @@ export class BaseMapObject extends BaseEntity { tags: string[] = [] @Property({ type: 'json' }) - pivotPoints: { x: number; y: number; }[] = [] + pivotPoints: { x: number; y: number }[] = [] @Property({ type: 'decimal', precision: 10, scale: 2 }) originX = 0 @@ -64,7 +64,7 @@ export class BaseMapObject extends BaseEntity { return this.tags } - setPivotPoints(pivotPoints: { x: number; y: number; }[]) { + setPivotPoints(pivotPoints: { x: number; y: number }[]) { this.pivotPoints = pivotPoints return this } diff --git a/src/events/gameMaster/assetManager/mapObject/update.ts b/src/events/gameMaster/assetManager/mapObject/update.ts index 0bc1495..c2bed1d 100644 --- a/src/events/gameMaster/assetManager/mapObject/update.ts +++ b/src/events/gameMaster/assetManager/mapObject/update.ts @@ -7,7 +7,7 @@ type Payload = { id: UUID name: string tags: string[] - pivotPoints: { x: number; y: number; }[] + pivotPoints: { x: number; y: number }[] originX: number originY: number frameRate: number diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index b518836..eef22ca 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -1,12 +1,13 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { MapEventTileWithTeleport } from '@/application/types' +import type { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' import MapCharacter from '@/models/mapCharacter' import MapEventTileRepository from '@/repositories/mapEventTileRepository' import CharacterService from '@/services/characterMoveService' +import characterMoveService from '@/services/characterMoveService' import TeleportService from '@/services/characterTeleportService' -import type {Character} from "@/entities/character"; export default class CharacterMove extends BaseEvent { private readonly characterService = CharacterService @@ -116,7 +117,7 @@ export default class CharacterMove extends BaseEvent { currentTile = path[i] nextTile = path[i + 1] - if (!currentTile || !nextTile || !this.isValidStep(currentTile, nextTile)) { + if (!currentTile || !nextTile || !characterMoveService.isValidStep(currentTile, nextTile)) { return } @@ -127,11 +128,11 @@ export default class CharacterMove extends BaseEvent { .setPositionY(nextTile.positionY) // Check for map events at the next tile - const mapEventTile = await this.checkMapEvents(character, nextTile) + const mapEventTile = await characterMoveService.checkMapEvents(character.getMap().getId(), nextTile) if (mapEventTile) { if (mapEventTile.type === 'BLOCK') break if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) { - await this.handleTeleportMapEventTile(mapEventTile as MapEventTileWithTeleport) + await characterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport) return } } @@ -154,43 +155,12 @@ export default class CharacterMove extends BaseEvent { lastMoveTime = Date.now() } } finally { - if (mapCharacter.isMoving && mapCharacter.currentPath === path) { + if (mapCharacter.isMoving) { this.finalizeMovement(mapCharacter) } } } - private isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean { - return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1 - } - - private async checkMapEvents(character: any, nextTile: { positionX: number; positionY: number }) { - const mapEventTileRepository = new MapEventTileRepository() - return mapEventTileRepository.getEventTileByMapIdAndPosition(character.getMap().getId(), Math.floor(nextTile.positionX), Math.floor(nextTile.positionY)) - } - - private async handleTeleportMapEventTile(mapEventTile: MapEventTileWithTeleport): Promise { - const teleport = mapEventTile.getTeleport() - if (teleport) { - await TeleportService.teleportCharacter(this.socket.characterId!, { - targetMapId: teleport.getToMap().getId(), - targetX: teleport.getToPositionX(), - targetY: teleport.getToPositionY(), - rotation: teleport.getToRotation() - }) - } - } - - private broadcastMovement(character: Character, isMoving: boolean): void { - this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { - characterId: character.id, - positionX: character.getPositionX(), - positionY: character.getPositionY(), - rotation: character.getRotation(), - isMoving - }) - } - private finalizeMovement(mapCharacter: MapCharacter): void { // Clear any existing timeout if (this.movementTimeout) { @@ -212,4 +182,14 @@ export default class CharacterMove extends BaseEvent { } }, this.STEP_DELAY * 2) // Increased delay to ensure all movement processing is complete } + + private broadcastMovement(character: Character, isMoving: boolean): void { + this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { + characterId: character.id, + positionX: character.getPositionX(), + positionY: character.getPositionY(), + rotation: character.getRotation(), + isMoving + }) + } } diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index 92756f9..58e32f4 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -1,7 +1,10 @@ import { BaseService } from '@/application/base/baseService' import config from '@/application/config' +import type { MapEventTileWithTeleport, UUID } from '@/application/types' import { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' +import MapEventTileRepository from '@/repositories/mapEventTileRepository' +import TeleportService from '@/services/characterTeleportService' type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } @@ -225,6 +228,27 @@ class CharacterMoveService extends BaseService { .filter((pos) => this.isValidPosition(pos, grid, end)) } + isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean { + return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1 + } + + async checkMapEvents(map_id: UUID, nextTile: { positionX: number; positionY: number }) { + const mapEventTileRepository = new MapEventTileRepository() + return mapEventTileRepository.getEventTileByMapIdAndPosition(map_id, nextTile.positionX, nextTile.positionY) + } + + async handleTeleportMapEventTile(character_id: UUID, mapEventTile: MapEventTileWithTeleport): Promise { + const teleport = mapEventTile.getTeleport() + if (teleport) { + await TeleportService.teleportCharacter(character_id, { + targetMapId: teleport.getToMap().getId(), + targetX: teleport.getToPositionX(), + targetY: teleport.getToPositionY(), + rotation: teleport.getToRotation() + }) + } + } + private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { return ( pos.positionX >= 0 && @@ -280,10 +304,6 @@ class CharacterMoveService extends BaseService { distance: requestDistance } } - - public isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean { - return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1 - } } export default new CharacterMoveService() From daeb232d3b875625f137d1bc75bc752adca61f4d Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sat, 15 Feb 2025 22:30:22 +0100 Subject: [PATCH 03/43] Silly typescript --- src/services/characterTeleportService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/characterTeleportService.ts b/src/services/characterTeleportService.ts index f6f1b00..0402305 100644 --- a/src/services/characterTeleportService.ts +++ b/src/services/characterTeleportService.ts @@ -36,7 +36,7 @@ class CharacterTeleportService { const existingCharacter = !options.isInitialJoin && MapManager.getCharacterById(characterId) const mapCharacter = options.isInitialJoin - ? new MapCharacter(options.character) + ? new MapCharacter(options.character!) : existingCharacter || (() => { this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`) @@ -83,7 +83,7 @@ class CharacterTeleportService { targetMap.addCharacter(mapCharacter.getCharacter()) const map = await mapRepository.getById(options.targetMapId) - await mapRepository.getEntityManager().populate(map, mapRepository.POPULATE_TELEPORT as any) + await mapRepository.getEntityManager().populate(map!, mapRepository.POPULATE_TELEPORT as any) // Notify clients io.in(options.targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter) From cbd6e2c3075f687ebdea6b25fdb789e5e33c5548 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 00:59:30 +0100 Subject: [PATCH 04/43] Minor improvements --- src/events/map/characterMove.ts | 29 +++++++++------------------- src/services/characterMoveService.ts | 7 +++++++ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index eef22ca..15083a3 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -4,13 +4,9 @@ import type { MapEventTileWithTeleport } from '@/application/types' import type { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' import MapCharacter from '@/models/mapCharacter' -import MapEventTileRepository from '@/repositories/mapEventTileRepository' -import CharacterService from '@/services/characterMoveService' -import characterMoveService from '@/services/characterMoveService' -import TeleportService from '@/services/characterTeleportService' +import CharacterMoveService from '@/services/characterMoveService' export default class CharacterMove extends BaseEvent { - private readonly characterService = CharacterService private readonly STEP_DELAY = 100 private readonly THROTTLE_DELAY = 230 private readonly MAX_REQUEST_DISTANCE = 30 // Maximum allowed distance for movement requests @@ -25,7 +21,6 @@ export default class CharacterMove extends BaseEvent { try { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) if (!mapCharacter?.getCharacter()) { - this.logger.error('map:character:move error: Character not found or not initialized') return } @@ -40,7 +35,7 @@ export default class CharacterMove extends BaseEvent { } // Validate current position against last known position - const movementValidation = this.characterService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) + const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y) @@ -49,7 +44,7 @@ export default class CharacterMove extends BaseEvent { } // Validate requested position distance - const requestValidation = this.characterService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE) + const requestValidation = CharacterMoveService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE) if (!requestValidation.isValid) { this.logger.warn(`Invalid movement distance detected: ${this.socket.characterId}`) @@ -65,13 +60,13 @@ export default class CharacterMove extends BaseEvent { } // Cancel any ongoing movement - this.cancelCurrentMovement(mapCharacter) + CharacterMoveService.cancelCurrentMovement(mapCharacter) // Update last known position this.lastKnownPosition = { x: currentX, y: currentY } // Calculate path to target position - const path = await this.characterService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) + const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { return @@ -86,12 +81,6 @@ export default class CharacterMove extends BaseEvent { } } - private cancelCurrentMovement(mapCharacter: MapCharacter): void { - if (!mapCharacter.isMoving) return - mapCharacter.isMoving = false - mapCharacter.currentPath = null - } - private async moveAlongPath(mapCharacter: MapCharacter): Promise { const character = mapCharacter.getCharacter() const path = mapCharacter.currentPath @@ -117,22 +106,22 @@ export default class CharacterMove extends BaseEvent { currentTile = path[i] nextTile = path[i + 1] - if (!currentTile || !nextTile || !characterMoveService.isValidStep(currentTile, nextTile)) { + if (!currentTile || !nextTile || !CharacterMoveService.isValidStep(currentTile, nextTile)) { return } // Update character rotation and position in a single operation character - .setRotation(CharacterService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY)) + .setRotation(CharacterMoveService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY)) .setPositionX(nextTile.positionX) .setPositionY(nextTile.positionY) // Check for map events at the next tile - const mapEventTile = await characterMoveService.checkMapEvents(character.getMap().getId(), nextTile) + const mapEventTile = await CharacterMoveService.checkMapEvents(character.getMap().getId(), nextTile) if (mapEventTile) { if (mapEventTile.type === 'BLOCK') break if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) { - await characterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport) + await CharacterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport) return } } diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index 58e32f4..8a90a6a 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -5,6 +5,7 @@ import { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' import MapEventTileRepository from '@/repositories/mapEventTileRepository' import TeleportService from '@/services/characterTeleportService' +import MapCharacter from "@/models/mapCharacter"; type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } @@ -276,6 +277,12 @@ class CharacterMoveService extends BaseService { return path } + public cancelCurrentMovement(mapCharacter: MapCharacter): void { + if (!mapCharacter.isMoving) return + mapCharacter.isMoving = false + mapCharacter.currentPath = null + } + public validateMovementDistance( currentX: number, currentY: number, From 17fa2a8f6e7ec273c549a0b3abc79deac7d9e866 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 01:29:24 +0100 Subject: [PATCH 05/43] More movement improvements --- src/events/map/characterMove.ts | 104 +++++++++++++-------------- src/services/characterMoveService.ts | 18 +++-- 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 15083a3..961b061 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -1,7 +1,6 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { MapEventTileWithTeleport } from '@/application/types' -import type { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' import MapCharacter from '@/models/mapCharacter' import CharacterMoveService from '@/services/characterMoveService' @@ -9,9 +8,10 @@ import CharacterMoveService from '@/services/characterMoveService' export default class CharacterMove extends BaseEvent { private readonly STEP_DELAY = 100 private readonly THROTTLE_DELAY = 230 - private readonly MAX_REQUEST_DISTANCE = 30 // Maximum allowed distance for movement requests + private readonly MAX_REQUEST_DISTANCE = 30 private movementTimeout: NodeJS.Timeout | null = null private lastKnownPosition: { x: number; y: number } | null = null + private isProcessingMove = false public listen(): void { this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this)) @@ -34,12 +34,15 @@ export default class CharacterMove extends BaseEvent { return } + // Stop any existing movement before starting a new one + await this.stopExistingMovement(mapCharacter) + // Validate current position against last known position const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y) - this.broadcastMovement(character, false) + CharacterMoveService.broadcastMovement(character, false) return } @@ -51,53 +54,59 @@ export default class CharacterMove extends BaseEvent { return } - // If character is already moving to the same target position, ignore the request - if (mapCharacter.isMoving && mapCharacter.currentPath?.length) { - const lastPathPoint = mapCharacter.currentPath[mapCharacter.currentPath.length - 1] - if (lastPathPoint && Math.abs(lastPathPoint.positionX - positionX) < 1 && Math.abs(lastPathPoint.positionY - positionY) < 1) { - return - } - } - - // Cancel any ongoing movement - CharacterMoveService.cancelCurrentMovement(mapCharacter) - - // Update last known position - this.lastKnownPosition = { x: currentX, y: currentY } - - // Calculate path to target position const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { return } - // Start new movement + // Set new movement state mapCharacter.isMoving = true mapCharacter.currentPath = path + this.isProcessingMove = true + + // Start the movement await this.moveAlongPath(mapCharacter) } catch (error: any) { this.logger.error('map:character:move error: ' + error.message) + } finally { + this.isProcessingMove = false } } + private async stopExistingMovement(mapCharacter: MapCharacter): Promise { + // Clear existing movement timeout + if (this.movementTimeout) { + clearTimeout(this.movementTimeout) + this.movementTimeout = null + } + + // Wait for any ongoing movement processing to complete + if (this.isProcessingMove) { + await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) + } + + // Only clear the path, keep isMoving state for continuous animation + mapCharacter.currentPath = null + } + private async moveAlongPath(mapCharacter: MapCharacter): Promise { const character = mapCharacter.getCharacter() const path = mapCharacter.currentPath - if (!path?.length) return + if (!path?.length || !character) return let lastMoveTime = Date.now() let currentTile, nextTile + const movementId = Date.now() // Unique identifier for this movement sequence try { for (let i = 0; i < path.length - 1; i++) { + // Check if this movement sequence is still valid if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) { - this.broadcastMovement(character, false) return } - // Ensure minimum time between moves using a single Date.now() call const timeSinceLastMove = Date.now() - lastMoveTime if (timeSinceLastMove < this.STEP_DELAY) { await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY - timeSinceLastMove)) @@ -110,14 +119,15 @@ export default class CharacterMove extends BaseEvent { return } - // Update character rotation and position in a single operation + // Update character position and rotation character .setRotation(CharacterMoveService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY)) .setPositionX(nextTile.positionX) .setPositionY(nextTile.positionY) - // Check for map events at the next tile + // Check for map events const mapEventTile = await CharacterMoveService.checkMapEvents(character.getMap().getId(), nextTile) + if (mapEventTile) { if (mapEventTile.type === 'BLOCK') break if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) { @@ -126,17 +136,15 @@ export default class CharacterMove extends BaseEvent { } } - // Broadcast movement + // Only broadcast if this is still the current movement if (mapCharacter.isMoving && mapCharacter.currentPath === path) { - this.broadcastMovement(character, true) + CharacterMoveService.broadcastMovement(character, true) } - // Apply movement delay between steps if (i < path.length - 2) { await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) } - // Update last known position and move time this.lastKnownPosition = { x: nextTile.positionX, y: nextTile.positionY @@ -144,41 +152,31 @@ export default class CharacterMove extends BaseEvent { lastMoveTime = Date.now() } } finally { - if (mapCharacter.isMoving) { - this.finalizeMovement(mapCharacter) + // Only finalize if this movement is still active + if (mapCharacter.isMoving && mapCharacter.currentPath === path) { + await this.finalizeMovement(mapCharacter) } } } - private finalizeMovement(mapCharacter: MapCharacter): void { - // Clear any existing timeout + private async finalizeMovement(mapCharacter: MapCharacter): Promise { if (this.movementTimeout) { clearTimeout(this.movementTimeout) } - // Clear the current path immediately mapCharacter.currentPath = null - // Set new timeout for movement state cleanup - this.movementTimeout = setTimeout(() => { - // Ensure the character is still in a valid state - if (!mapCharacter.isMoving || !mapCharacter.currentPath) { - mapCharacter.isMoving = false - // Save the final position and broadcast it - mapCharacter.savePosition().then(() => { - this.broadcastMovement(mapCharacter.character, false) - }) - } - }, this.STEP_DELAY * 2) // Increased delay to ensure all movement processing is complete - } + const character = mapCharacter.getCharacter() + if (character) { + await mapCharacter.savePosition() - private broadcastMovement(character: Character, isMoving: boolean): void { - this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { - characterId: character.id, - positionX: character.getPositionX(), - positionY: character.getPositionY(), - rotation: character.getRotation(), - isMoving - }) + // Set a timeout to stop movement animation if no new movement is received + this.movementTimeout = setTimeout(() => { + if (!mapCharacter.currentPath) { + mapCharacter.isMoving = false + CharacterMoveService.broadcastMovement(character, false) + } + }, this.THROTTLE_DELAY) // Use THROTTLE_DELAY to match input timing + } } } diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index 8a90a6a..eab632f 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -1,11 +1,13 @@ import { BaseService } from '@/application/base/baseService' import config from '@/application/config' +import { SocketEvent } from '@/application/enums' import type { MapEventTileWithTeleport, UUID } from '@/application/types' import { Character } from '@/entities/character' import MapManager from '@/managers/mapManager' +import SocketManager from '@/managers/socketManager' import MapEventTileRepository from '@/repositories/mapEventTileRepository' import TeleportService from '@/services/characterTeleportService' -import MapCharacter from "@/models/mapCharacter"; +import type { Server } from 'socket.io' type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } @@ -75,6 +77,8 @@ class PriorityQueue { } class CharacterMoveService extends BaseService { + private io: Server = SocketManager.getIO() + // Rotation lookup table for better performance private readonly ROTATION_MAP = { diagonal: { @@ -277,10 +281,14 @@ class CharacterMoveService extends BaseService { return path } - public cancelCurrentMovement(mapCharacter: MapCharacter): void { - if (!mapCharacter.isMoving) return - mapCharacter.isMoving = false - mapCharacter.currentPath = null + public broadcastMovement(character: Character, isMoving: boolean): void { + this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { + characterId: character.id, + positionX: character.getPositionX(), + positionY: character.getPositionY(), + rotation: character.getRotation(), + isMoving + }) } public validateMovementDistance( From 9f84247839cd078e40dc873a8228da7f82eaca9d Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 01:35:25 +0100 Subject: [PATCH 06/43] Stop moving if path is invalid --- src/events/map/characterMove.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 961b061..6703a86 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -57,12 +57,16 @@ export default class CharacterMove extends BaseEvent { const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { + // Ensure movement state is cleaned up for invalid paths + mapCharacter.isMoving = false + mapCharacter.currentPath = null + CharacterMoveService.broadcastMovement(character, false) return } - // Set new movement state - mapCharacter.isMoving = true + // Only set movement state if we have a valid path mapCharacter.currentPath = path + mapCharacter.isMoving = true this.isProcessingMove = true // Start the movement From 161a9795bc396b2b9855baad50ee45af0a7c58fb Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:12:01 +0100 Subject: [PATCH 07/43] Near perfect movement --- src/events/map/characterMove.ts | 26 +--- src/services/characterMoveService.ts | 188 +++++++++++++-------------- 2 files changed, 99 insertions(+), 115 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 6703a86..c679fb6 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -7,7 +7,7 @@ import CharacterMoveService from '@/services/characterMoveService' export default class CharacterMove extends BaseEvent { private readonly STEP_DELAY = 100 - private readonly THROTTLE_DELAY = 230 + private readonly THROTTLE_DELAY = 200 private readonly MAX_REQUEST_DISTANCE = 30 private movementTimeout: NodeJS.Timeout | null = null private lastKnownPosition: { x: number; y: number } | null = null @@ -20,9 +20,7 @@ export default class CharacterMove extends BaseEvent { private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise { try { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) - if (!mapCharacter?.getCharacter()) { - return - } + if (!mapCharacter?.getCharacter()) return const character = mapCharacter.getCharacter() const currentX = character.getPositionX() @@ -30,14 +28,12 @@ export default class CharacterMove extends BaseEvent { // Enhanced throttling with position tracking const throttleKey = `movement_${this.socket.characterId}` - if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) { - return - } + if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) return // Stop any existing movement before starting a new one await this.stopExistingMovement(mapCharacter) - // Validate current position against last known position + // Validate movement const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { @@ -46,30 +42,21 @@ export default class CharacterMove extends BaseEvent { return } - // Validate requested position distance - const requestValidation = CharacterMoveService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE) - - if (!requestValidation.isValid) { - this.logger.warn(`Invalid movement distance detected: ${this.socket.characterId}`) - return - } - + // Calculate and validate path const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { - // Ensure movement state is cleaned up for invalid paths mapCharacter.isMoving = false mapCharacter.currentPath = null CharacterMoveService.broadcastMovement(character, false) return } - // Only set movement state if we have a valid path + // Start movement mapCharacter.currentPath = path mapCharacter.isMoving = true this.isProcessingMove = true - // Start the movement await this.moveAlongPath(mapCharacter) } catch (error: any) { this.logger.error('map:character:move error: ' + error.message) @@ -102,7 +89,6 @@ export default class CharacterMove extends BaseEvent { let lastMoveTime = Date.now() let currentTile, nextTile - const movementId = Date.now() // Unique identifier for this movement sequence try { for (let i = 0; i < path.length - 1; i++) { diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index eab632f..dcf4e56 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -12,70 +12,6 @@ import type { Server } from 'socket.io' type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } -class PriorityQueue { - private items: T[] = [] - constructor(private compare: (a: T, b: T) => number) {} - - enqueue(item: T): void { - this.items.push(item) - this.siftUp(this.items.length - 1) - } - - dequeue(): T | undefined { - if (this.items.length === 0) return undefined - - const result = this.items[0] - const last = this.items.pop() - - if (this.items.length > 0 && last !== undefined) { - this.items[0] = last - this.siftDown(0) - } - - return result - } - - get length(): number { - return this.items.length - } - - private siftUp(index: number): void { - while (index > 0) { - const parentIndex = Math.floor((index - 1) / 2) - if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) { - ;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!] - index = parentIndex - } else { - break - } - } - } - - private siftDown(index: number): void { - const length = this.items.length - while (true) { - let minIndex = index - const leftChild = 2 * index + 1 - const rightChild = 2 * index + 2 - - if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) { - minIndex = leftChild - } - if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) { - minIndex = rightChild - } - - if (minIndex === index) break - ;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!] - index = minIndex - } - } - - clear(): void { - this.items = [] - } -} - class CharacterMoveService extends BaseService { private io: Server = SocketManager.getIO() @@ -182,12 +118,14 @@ class CharacterMoveService extends BaseService { private findPath(start: Position, end: Position, grid: number[][]): Node[] { const openList = new PriorityQueue((a, b) => a.f - b.f) const closedSet = new Set() - const MAX_ITERATIONS = 1000 // Prevent infinite loops for impossible paths + const MAX_ITERATIONS = 1000 let iterations = 0 const getKey = (p: Position) => `${p.positionX},${p.positionY}` + const gScoreMap = new Map() - openList.enqueue({ ...start, g: 0, h: 0, f: 0 }) + openList.enqueue({ ...start, g: 0, h: this.getDistance(start, end), f: this.getDistance(start, end) }) + gScoreMap.set(getKey(start), 0) try { while (openList.length > 0 && iterations < MAX_ITERATIONS) { @@ -199,28 +137,34 @@ class CharacterMoveService extends BaseService { return this.reconstructPath(current) } - closedSet.add(getKey(current)) + const currentKey = getKey(current) + closedSet.add(currentKey) const neighbors = this.getValidNeighbors(current, grid, end) for (const neighbor of neighbors) { - if (closedSet.has(getKey(neighbor))) continue + const neighborKey = getKey(neighbor) + if (closedSet.has(neighborKey)) continue - const g = current.g + this.getDistance(current, neighbor) - const h = this.getDistance(neighbor, end) - const f = g + h + const tentativeGScore = current.g + this.getDistance(current, neighbor) - const node: Node = { ...neighbor, g, h, f, parent: current } - openList.enqueue(node) + if (!gScoreMap.has(neighborKey) || tentativeGScore < gScoreMap.get(neighborKey)!) { + gScoreMap.set(neighborKey, tentativeGScore) + const h = this.getDistance(neighbor, end) + const f = tentativeGScore + h + + const node: Node = { ...neighbor, g: tentativeGScore, h, f, parent: current } + openList.enqueue(node) + } } } this.logger.warn(`Path not found after ${iterations} iterations`) - return [] // No path found + return [] } finally { - // Clean up resources - while (openList.length > 0) openList.dequeue() + openList.clear() closedSet.clear() + gScoreMap.clear() } } @@ -233,6 +177,14 @@ class CharacterMoveService extends BaseService { .filter((pos) => this.isValidPosition(pos, grid, end)) } + private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { + if (pos.positionX < 0 || pos.positionY < 0 || pos.positionX >= grid[0]!.length || pos.positionY >= grid.length) { + return false + } + + return grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY) + } + isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean { return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1 } @@ -254,16 +206,6 @@ class CharacterMoveService extends BaseService { } } - private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { - return ( - pos.positionX >= 0 && - pos.positionY >= 0 && - pos.positionX < grid[0]!.length && - pos.positionY < grid.length && - (grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY)) - ) - } - private getDistance(a: Position, b: Position): number { const dx = Math.abs(a.positionX - b.positionX) const dy = Math.abs(a.positionY - b.positionY) @@ -302,7 +244,7 @@ class CharacterMoveService extends BaseService { return { isValid: true, maxAllowedDistance: 0, actualDistance: 0 } } - const maxAllowedDistance = Math.ceil((stepDelay / 1000) * 2) + const maxAllowedDistance = Math.ceil((stepDelay / 1000) * (config.ALLOW_DIAGONAL_MOVEMENT ? 2.5 : 2)) const actualDistance = Math.sqrt(Math.pow(currentX - lastKnownPosition.x, 2) + Math.pow(currentY - lastKnownPosition.y, 2)) return { @@ -311,14 +253,70 @@ class CharacterMoveService extends BaseService { actualDistance } } - - public validateRequestDistance(currentX: number, currentY: number, targetX: number, targetY: number, maxRequestDistance: number): { isValid: boolean; distance: number } { - const requestDistance = Math.sqrt(Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)) - return { - isValid: requestDistance <= maxRequestDistance, - distance: requestDistance - } - } } export default new CharacterMoveService() + +class PriorityQueue { + private items: T[] = [] + constructor(private compare: (a: T, b: T) => number) {} + + enqueue(item: T): void { + this.items.push(item) + this.siftUp(this.items.length - 1) + } + + dequeue(): T | undefined { + if (this.items.length === 0) return undefined + + const result = this.items[0] + const last = this.items.pop() + + if (this.items.length > 0 && last !== undefined) { + this.items[0] = last + this.siftDown(0) + } + + return result + } + + get length(): number { + return this.items.length + } + + private siftUp(index: number): void { + while (index > 0) { + const parentIndex = Math.floor((index - 1) / 2) + if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) { + ;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!] + index = parentIndex + } else { + break + } + } + } + + private siftDown(index: number): void { + const length = this.items.length + while (true) { + let minIndex = index + const leftChild = 2 * index + 1 + const rightChild = 2 * index + 2 + + if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) { + minIndex = leftChild + } + if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) { + minIndex = rightChild + } + + if (minIndex === index) break + ;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!] + index = minIndex + } + } + + clear(): void { + this.items = [] + } +} From 049456cc40f5cc6173878c4bf899d093dad5d54b Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:15:18 +0100 Subject: [PATCH 08/43] npm update --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ce232b..91edb6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1932,9 +1932,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.1.tgz", - "integrity": "sha512-bVQGR4ARM+wRJSw66114AKwO8SwS2ZF5TIvwQ9NL6Iepq6f8jnG8EjMMXL8J1pyR1eNz5bOikPeED/8ErLU6FQ==", + "version": "5.41.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz", + "integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", From 562935c6e83a25bc8fe71f6819915659f35326a8 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:19:14 +0100 Subject: [PATCH 09/43] Send new location as array instead of object --- src/events/map/characterMove.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index c679fb6..85efc72 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -17,7 +17,7 @@ export default class CharacterMove extends BaseEvent { this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this)) } - private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise { + private async handleEvent([positionX, positionY]: [number, number]): Promise { try { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) if (!mapCharacter?.getCharacter()) return From 67984f3e89c9b96835a4a6e1b95fbb7d5adc66c9 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:20:00 +0100 Subject: [PATCH 10/43] Removed if check since character is always in a map --- src/models/mapCharacter.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/models/mapCharacter.ts b/src/models/mapCharacter.ts index 5855a47..8fec98b 100644 --- a/src/models/mapCharacter.ts +++ b/src/models/mapCharacter.ts @@ -38,13 +38,11 @@ class MapCharacter { await this.savePosition() // Leave map and remove from manager - if (this.character.map) { - socket.leave(this.character.map.id) - MapManager.removeCharacter(this.character.id) + socket.leave(this.character.map.id) + MapManager.removeCharacter(this.character.id) - // Notify map players - io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id) - } + // Notify map players + io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id) // Notify all players io.emit(SocketEvent.CHARACTER_DISCONNECT, this.character.id) From d68d307895e8f96c149444a029f51e610b275dd9 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:29:33 +0100 Subject: [PATCH 11/43] Send new location as array instead of object --- src/services/characterMoveService.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index dcf4e56..cf40b5e 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -224,13 +224,7 @@ class CharacterMoveService extends BaseService { } public broadcastMovement(character: Character, isMoving: boolean): void { - this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, { - characterId: character.id, - positionX: character.getPositionX(), - positionY: character.getPositionY(), - rotation: character.getRotation(), - isMoving - }) + this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, [character.id, character.getPositionX(), character.getPositionY(), character.getRotation(), isMoving]) } public validateMovementDistance( From f2dd1a2ffef09f0f6a2052d8f6023b57bc2d6954 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 18:16:17 +0100 Subject: [PATCH 12/43] Minor movement improvements --- src/events/map/characterMove.ts | 5 +++++ src/services/characterMoveService.ts | 7 +++---- src/services/characterTeleportService.ts | 9 ++------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 85efc72..c3ebf28 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -121,6 +121,11 @@ export default class CharacterMove extends BaseEvent { if (mapEventTile) { if (mapEventTile.type === 'BLOCK') break if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) { + // Force clear movement state before teleport + mapCharacter.isMoving = false + mapCharacter.currentPath = null + this.lastKnownPosition = null // Reset last known position + await CharacterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport) return } diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index cf40b5e..9b3a56a 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -13,8 +13,6 @@ type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } class CharacterMoveService extends BaseService { - private io: Server = SocketManager.getIO() - // Rotation lookup table for better performance private readonly ROTATION_MAP = { diagonal: { @@ -223,8 +221,9 @@ class CharacterMoveService extends BaseService { return path } - public broadcastMovement(character: Character, isMoving: boolean): void { - this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, [character.id, character.getPositionX(), character.getPositionY(), character.getRotation(), isMoving]) + public broadcastMovement(character: Character, isMoving: boolean = false): void { + const io: Server = SocketManager.getIO() + io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, [character.id, character.getPositionX(), character.getPositionY(), character.getRotation(), isMoving]) } public validateMovementDistance( diff --git a/src/services/characterTeleportService.ts b/src/services/characterTeleportService.ts index 0402305..d332485 100644 --- a/src/services/characterTeleportService.ts +++ b/src/services/characterTeleportService.ts @@ -6,6 +6,7 @@ import MapManager from '@/managers/mapManager' import SocketManager from '@/managers/socketManager' import MapCharacter from '@/models/mapCharacter' import MapRepository from '@/repositories/mapRepository' +import CharacterMoveService from '@/services/characterMoveService' interface TeleportOptions { targetMapId: UUID @@ -61,13 +62,7 @@ class CharacterTeleportService { // If the current map is the target map and we are not joining, send move event if (currentMapId === options.targetMapId && !options.isInitialJoin) { // If the current map is the target map, send move event - io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_MOVE, { - characterId: mapCharacter.character.id, - positionX: options.targetX, - positionY: options.targetY, - rotation: options.rotation ?? 0, - isMoving: false - }) + CharacterMoveService.broadcastMovement(mapCharacter.character, false) return true } From 1191e6bf55f6e04cc8a58bb2e43f79ddc5dfcf86 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 18:47:44 +0100 Subject: [PATCH 13/43] Small improvement teleports --- src/managers/mapManager.ts | 4 ---- src/models/mapCharacter.ts | 6 +++++- src/services/characterTeleportService.ts | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/managers/mapManager.ts b/src/managers/mapManager.ts index 23705fa..4853ceb 100644 --- a/src/managers/mapManager.ts +++ b/src/managers/mapManager.ts @@ -44,10 +44,6 @@ class MapManager { .find((char) => char.character.id === characterId) ?? null ) } - - public removeCharacter(characterId: UUID): void { - Object.values(this.maps).forEach((map) => map.removeCharacter(characterId)) - } } export default new MapManager() diff --git a/src/models/mapCharacter.ts b/src/models/mapCharacter.ts index 8fec98b..560fa89 100644 --- a/src/models/mapCharacter.ts +++ b/src/models/mapCharacter.ts @@ -39,7 +39,11 @@ class MapCharacter { // Leave map and remove from manager socket.leave(this.character.map.id) - MapManager.removeCharacter(this.character.id) + + const map = MapManager.getMapById(this.character.map.id) + if (map) { + await map.removeCharacter(this.character.id) + } // Notify map players io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id) diff --git a/src/services/characterTeleportService.ts b/src/services/characterTeleportService.ts index d332485..e3cf0e2 100644 --- a/src/services/characterTeleportService.ts +++ b/src/services/characterTeleportService.ts @@ -48,6 +48,7 @@ class CharacterTeleportService { try { const currentMapId = mapCharacter.character.map?.id + const currentMap = MapManager.getMapById(currentMapId!) const io = SocketManager.getIO() // Update character position and map @@ -67,9 +68,9 @@ class CharacterTeleportService { } // Handle current map cleanup - if (currentMapId) { + if (currentMapId && currentMap) { socket.leave(currentMapId) - MapManager.removeCharacter(characterId) + await currentMap.removeCharacter(characterId) io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId) } From 4cf87536ceb2309425da286b2d1d7b5b2b4f0089 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 18:54:20 +0100 Subject: [PATCH 14/43] More teleport improvements --- src/managers/mapManager.ts | 20 ++-- src/models/loadedMap.ts | 21 ++-- src/services/characterTeleportService.ts | 119 +++++++++++++---------- 3 files changed, 90 insertions(+), 70 deletions(-) diff --git a/src/managers/mapManager.ts b/src/managers/mapManager.ts index 4853ceb..4f846a8 100644 --- a/src/managers/mapManager.ts +++ b/src/managers/mapManager.ts @@ -15,16 +15,20 @@ class MapManager { await mapRepository.getEntityManager().populate(maps, mapRepository.POPULATE_ALL as never[]) await Promise.all(maps.map((map) => this.loadMap(map))) - this.logger.info(`Map manager loaded with ${Object.keys(this.maps).length} maps`) } - public async loadMap(map: Map): Promise { - this.maps[map.id] = new LoadedMap(map) + public async loadMap(map: Map): Promise { + const loadedMap = new LoadedMap(map) + this.maps[map.id] = loadedMap this.logger.info(`Map ID ${map.id} loaded`) + return loadedMap } public unloadMap(mapId: UUID): void { + const map = this.maps[mapId] + if (!map) return + delete this.maps[mapId] this.logger.info(`Map ID ${mapId} unloaded`) } @@ -38,11 +42,11 @@ class MapManager { } public getCharacterById(characterId: UUID): MapCharacter | null { - return ( - Object.values(this.maps) - .flatMap((map) => map.getCharactersInMap()) - .find((char) => char.character.id === characterId) ?? null - ) + for (const map of Object.values(this.maps)) { + const character = map.getCharacterById(characterId) + if (character) return character + } + return null } } diff --git a/src/models/loadedMap.ts b/src/models/loadedMap.ts index f710eec..a2c0c7a 100644 --- a/src/models/loadedMap.ts +++ b/src/models/loadedMap.ts @@ -16,17 +16,23 @@ class LoadedMap { return this.map } - public addCharacter(character: Character) { + public addCharacter(character: Character): MapCharacter { + const existingCharacter = this.getCharacterById(character.id) + if (existingCharacter) { + return existingCharacter + } + const mapCharacter = new MapCharacter(character) this.characters.push(mapCharacter) + return mapCharacter } - public async removeCharacter(id: UUID) { + public async removeCharacter(id: UUID): Promise { const mapCharacter = this.getCharacterById(id) - if (mapCharacter) { - await mapCharacter.savePosition() - this.characters = this.characters.filter((c) => c.character.id !== id) - } + if (!mapCharacter) return + + await mapCharacter.savePosition() + this.characters = this.characters.filter((c) => c.character.id !== id) } public getCharacterById(id: UUID): MapCharacter | undefined { @@ -38,12 +44,11 @@ class LoadedMap { } public async getGrid(): Promise { - let grid: number[][] = Array.from({ length: this.map.height }, () => Array.from({ length: this.map.width }, () => 0)) + const grid: number[][] = Array.from({ length: this.map.height }, () => Array.from({ length: this.map.width }, () => 0)) const mapEventTileRepository = new MapEventTileRepository() const eventTiles = await mapEventTileRepository.getAll(this.map.id) - // Set the grid values based on the event tiles, these are strings eventTiles.forEach((eventTile) => { if (eventTile.type === 'BLOCK') { grid[eventTile.positionY]![eventTile.positionX] = 1 diff --git a/src/services/characterTeleportService.ts b/src/services/characterTeleportService.ts index e3cf0e2..cf1d0c9 100644 --- a/src/services/characterTeleportService.ts +++ b/src/services/characterTeleportService.ts @@ -21,72 +21,25 @@ class CharacterTeleportService { private readonly logger = Logger.type(LoggerType.GAME) public async teleportCharacter(characterId: UUID, options: TeleportOptions): Promise { - const mapRepository = new MapRepository() - const socket = SocketManager.getSocketByCharacterId(characterId) - const targetMap = MapManager.getMapById(options.targetMapId) - - if (!socket || !targetMap) { - this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`) - return false - } - - if (options.isInitialJoin && !options.character) { - this.logger.error('Initial join requires character data') - return false - } - - const existingCharacter = !options.isInitialJoin && MapManager.getCharacterById(characterId) - const mapCharacter = options.isInitialJoin - ? new MapCharacter(options.character!) - : existingCharacter || - (() => { - this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`) - return null - })() - - if (!mapCharacter) return false - try { + const { socket, targetMap, mapCharacter } = await this.validateTeleportRequest(characterId, options) + if (!socket || !targetMap || !mapCharacter) return false + const currentMapId = mapCharacter.character.map?.id const currentMap = MapManager.getMapById(currentMapId!) const io = SocketManager.getIO() // Update character position and map - await mapCharacter - .getCharacter() - .setPositionX(options.targetX) - .setPositionY(options.targetY) - .setRotation(options.rotation ?? 0) - .setMap(targetMap.getMap()) - .save() + await this.updateCharacterPosition(mapCharacter, options, targetMap) - // If the current map is the target map and we are not joining, send move event + // Handle same map teleport if (currentMapId === options.targetMapId && !options.isInitialJoin) { - // If the current map is the target map, send move event CharacterMoveService.broadcastMovement(mapCharacter.character, false) return true } - // Handle current map cleanup - if (currentMapId && currentMap) { - socket.leave(currentMapId) - await currentMap.removeCharacter(characterId) - io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId) - } - - // Join new map - socket.join(options.targetMapId) - targetMap.addCharacter(mapCharacter.getCharacter()) - - const map = await mapRepository.getById(options.targetMapId) - await mapRepository.getEntityManager().populate(map!, mapRepository.POPULATE_TELEPORT as any) - - // Notify clients - io.in(options.targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter) - socket.emit(SocketEvent.MAP_CHARACTER_TELEPORT, { - mapId: options.targetMapId, - characters: targetMap.getCharactersInMap() - }) + // Handle map transition + await this.handleMapTransition(socket, io, currentMapId, currentMap, options.targetMapId, targetMap, characterId, mapCharacter) return true } catch (error) { @@ -94,6 +47,64 @@ class CharacterTeleportService { return false } } + + private async validateTeleportRequest(characterId: UUID, options: TeleportOptions) { + const socket = SocketManager.getSocketByCharacterId(characterId) + const targetMap = MapManager.getMapById(options.targetMapId) + + if (!socket || !targetMap) { + this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`) + return { socket: null, targetMap: null, mapCharacter: null } + } + + if (options.isInitialJoin && !options.character) { + this.logger.error('Initial join requires character data') + return { socket, targetMap, mapCharacter: null } + } + + const existingCharacter = !options.isInitialJoin && MapManager.getCharacterById(characterId) + const mapCharacter = options.isInitialJoin ? new MapCharacter(options.character!) : existingCharacter || null + + if (!mapCharacter) { + this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`) + } + + return { socket, targetMap, mapCharacter } + } + + private async updateCharacterPosition(mapCharacter: MapCharacter, options: TeleportOptions, targetMap: any) { + await mapCharacter + .getCharacter() + .setPositionX(options.targetX) + .setPositionY(options.targetY) + .setRotation(options.rotation ?? 0) + .setMap(targetMap.getMap()) + .save() + } + + private async handleMapTransition(socket: any, io: any, currentMapId: UUID | undefined, currentMap: any, targetMapId: UUID, targetMap: any, characterId: UUID, mapCharacter: MapCharacter) { + // Clean up current map + if (currentMapId && currentMap) { + socket.leave(currentMapId) + await currentMap.removeCharacter(characterId) + io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId) + } + + // Join new map + socket.join(targetMapId) + targetMap.addCharacter(mapCharacter.getCharacter()) + + const mapRepository = new MapRepository() + const map = await mapRepository.getById(targetMapId) + await mapRepository.getEntityManager().populate(map!, mapRepository.POPULATE_TELEPORT as any) + + // Notify clients + io.in(targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter) + socket.emit(SocketEvent.MAP_CHARACTER_TELEPORT, { + mapId: targetMapId, + characters: targetMap.getCharactersInMap() + }) + } } export default new CharacterTeleportService() From f3e0d6e03a74e9c22a6a8eb46d0aefdb9d4e3241 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 19:16:50 +0100 Subject: [PATCH 15/43] Redundant checks --- src/services/characterTeleportService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/characterTeleportService.ts b/src/services/characterTeleportService.ts index cf1d0c9..646a6e7 100644 --- a/src/services/characterTeleportService.ts +++ b/src/services/characterTeleportService.ts @@ -25,8 +25,8 @@ class CharacterTeleportService { const { socket, targetMap, mapCharacter } = await this.validateTeleportRequest(characterId, options) if (!socket || !targetMap || !mapCharacter) return false - const currentMapId = mapCharacter.character.map?.id - const currentMap = MapManager.getMapById(currentMapId!) + const currentMapId = mapCharacter.character.map.id + const currentMap = MapManager.getMapById(currentMapId) const io = SocketManager.getIO() // Update character position and map From b173a993f7bdb0d9f4317a5cdb91cd7bee549195 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 21:18:01 +0100 Subject: [PATCH 16/43] Map editor teleport enhancements --- src/entities/base/map.ts | 4 +-- src/entities/map.ts | 39 ++++++++++++++++++++++ src/events/gameMaster/mapEditor/request.ts | 8 ++--- src/events/gameMaster/mapEditor/update.ts | 22 ++++++------ 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/entities/base/map.ts b/src/entities/base/map.ts index d40431b..fbda149 100644 --- a/src/entities/base/map.ts +++ b/src/entities/base/map.ts @@ -11,7 +11,7 @@ export class BaseMap extends BaseEntity { id = randomUUID() @Property() - name!: string + name: string = '' @Property() width = 10 @@ -19,7 +19,7 @@ export class BaseMap extends BaseEntity { @Property() height = 10 - @Property({ type: 'json', nullable: true }) + @Property({ type: 'json' }) tiles: Array> = [] @Property() diff --git a/src/entities/map.ts b/src/entities/map.ts index e7f6f4e..cdb3d29 100644 --- a/src/entities/map.ts +++ b/src/entities/map.ts @@ -2,6 +2,7 @@ import { BaseMap } from '@/entities/base/map' import { Entity } from '@mikro-orm/core' export type MapCacheT = ReturnType | {} +export type MapEditorMapT = ReturnType | {} @Entity() export class Map extends BaseMap { @@ -35,4 +36,42 @@ export class Map extends BaseMap { return {} } } + + public async mapEditorObject() { + try { + await this.getPlacedMapObjects().load() + await this.getMapEffects().load() + await this.getMapEventTiles().load() + + return { + id: this.getId(), + name: this.getName(), + width: this.getWidth(), + height: this.getHeight(), + tiles: this.getTiles(), + pvp: this.getPvp(), + createdAt: this.getCreatedAt(), + updatedAt: this.getUpdatedAt(), + placedMapObjects: this.getPlacedMapObjects(), + mapEffects: this.getMapEffects(), + mapEventTiles: this.getMapEventTiles().map((mapEventTile) => ({ + id: mapEventTile.getId(), + type: mapEventTile.getType(), + positionX: mapEventTile.getPositionX(), + positionY: mapEventTile.getPositionY(), + teleport: mapEventTile.getTeleport() + ? { + toMap: mapEventTile.getTeleport()?.getToMap().getId(), + toPositionX: mapEventTile.getTeleport()?.getToPositionX(), + toPositionY: mapEventTile.getTeleport()?.getToPositionY(), + toRotation: mapEventTile.getTeleport()?.getToRotation() + } + : undefined + })) + } + } catch (error) { + console.error(error) + return {} + } + } } diff --git a/src/events/gameMaster/mapEditor/request.ts b/src/events/gameMaster/mapEditor/request.ts index dc9fe93..c1885db 100644 --- a/src/events/gameMaster/mapEditor/request.ts +++ b/src/events/gameMaster/mapEditor/request.ts @@ -1,7 +1,7 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { UUID } from '@/application/types' -import { Map } from '@/entities/map' +import { type MapEditorMapT } from '@/entities/map' import MapRepository from '@/repositories/mapRepository' interface IPayload { @@ -13,7 +13,7 @@ export default class MapRequestEvent extends BaseEvent { this.socket.on(SocketEvent.GM_MAP_REQUEST, this.handleEvent.bind(this)) } - private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise { + private async handleEvent(data: IPayload, callback: (response: MapEditorMapT | null) => void): Promise { try { if (!(await this.isCharacterGM())) return @@ -34,9 +34,7 @@ export default class MapRequestEvent extends BaseEvent { await mapRepository.getEntityManager().populate(map, mapRepository.POPULATE_MAP_EDITOR as any) - // Remove map.mapEventTiles.teleport.toMap and add map.mapEventTiles.teleport.toMapId - - return callback(map) + return callback(await map.mapEditorObject()) } catch (error: any) { this.logger.error('gm:map:request error', error.message) return callback(null) diff --git a/src/events/gameMaster/mapEditor/update.ts b/src/events/gameMaster/mapEditor/update.ts index 1df390d..8989745 100644 --- a/src/events/gameMaster/mapEditor/update.ts +++ b/src/events/gameMaster/mapEditor/update.ts @@ -1,7 +1,7 @@ import { BaseEvent } from '@/application/base/baseEvent' import { MapEventTileType, SocketEvent } from '@/application/enums' import type { UUID } from '@/application/types' -import { Map } from '@/entities/map' +import { type MapEditorMapT } from '@/entities/map' import { MapEffect } from '@/entities/mapEffect' import { MapEventTile } from '@/entities/mapEventTile' import { MapEventTileTeleport } from '@/entities/mapEventTileTeleport' @@ -21,7 +21,7 @@ interface IPayload { positionX: number positionY: number teleport?: { - toMapId: string + toMap: string toPositionX: number toPositionY: number toRotation: number @@ -36,7 +36,7 @@ export default class MapUpdateEvent extends BaseEvent { this.socket.on(SocketEvent.GM_MAP_UPDATE, this.handleEvent.bind(this)) } - private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise { + private async handleEvent(data: IPayload, callback: (response: MapEditorMapT | null) => void): Promise { try { if (!(await this.isCharacterGM())) return @@ -80,17 +80,17 @@ export default class MapUpdateEvent extends BaseEvent { map.getMapEffects().removeAll() // Create and add new map event tiles - for (const tile of data.mapEventTiles) { - const mapEventTile = new MapEventTile().setType(tile.type).setPositionX(tile.positionX).setPositionY(tile.positionY).setMap(map) - if (tile.teleport) { - const toMap = await mapRepository.getById(tile.teleport.toMapId as UUID) + for (const eventTile of data.mapEventTiles) { + const mapEventTile = new MapEventTile().setMap(map).setType(eventTile.type).setPositionX(eventTile.positionX).setPositionY(eventTile.positionY) + if (eventTile.teleport) { + const toMap = await mapRepository.getById(eventTile.teleport.toMap as UUID) if (!toMap) continue const teleport = new MapEventTileTeleport() .setMapEventTile(mapEventTile) .setToMap(toMap) - .setToPositionX(tile.teleport.toPositionX) - .setToPositionY(tile.teleport.toPositionY) - .setToRotation(tile.teleport.toRotation) + .setToPositionX(eventTile.teleport.toPositionX) + .setToPositionY(eventTile.teleport.toPositionY) + .setToRotation(eventTile.teleport.toRotation) mapEventTile.setTeleport(teleport) } @@ -128,7 +128,7 @@ export default class MapUpdateEvent extends BaseEvent { mapManager.unloadMap(data.mapId) await mapManager.loadMap(map) - return callback(map) + return callback(await map.mapEditorObject()) } catch (error: any) { this.emitError(`gm:map:update error: ${error instanceof Error ? error.message + error.stack : String(error)}`) return callback(null) From 58d7e02de2110d0a2922fa4ba9b658f563ac2e53 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 21:20:25 +0100 Subject: [PATCH 17/43] Updated default values, added new init. migration --- src/entities/base/mapEventTile.ts | 4 ++-- src/migrations/.snapshot-game.json | 5 ++++- ...igration20250215152716.ts => Migration20250216202007.ts} | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) rename src/migrations/{Migration20250215152716.ts => Migration20250216202007.ts} (96%) diff --git a/src/entities/base/mapEventTile.ts b/src/entities/base/mapEventTile.ts index afc8e70..9b85382 100644 --- a/src/entities/base/mapEventTile.ts +++ b/src/entities/base/mapEventTile.ts @@ -17,10 +17,10 @@ export class BaseMapEventTile extends BaseEntity { type!: MapEventTileType @Property() - positionX!: number + positionX: number = 0 @Property() - positionY!: number + positionY: number = 0 @OneToOne({ eager: true }) teleport?: MapEventTileTeleport diff --git a/src/migrations/.snapshot-game.json b/src/migrations/.snapshot-game.json index 3a6735c..1ab2576 100644 --- a/src/migrations/.snapshot-game.json +++ b/src/migrations/.snapshot-game.json @@ -21,6 +21,7 @@ "primary": false, "nullable": false, "length": 255, + "default": "''", "mappedType": "string" }, "width": { @@ -51,7 +52,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "length": null, "mappedType": "json" }, @@ -233,6 +234,7 @@ "primary": false, "nullable": false, "length": null, + "default": "0", "mappedType": "integer" }, "position_y": { @@ -243,6 +245,7 @@ "primary": false, "nullable": false, "length": null, + "default": "0", "mappedType": "integer" }, "teleport_id": { diff --git a/src/migrations/Migration20250215152716.ts b/src/migrations/Migration20250216202007.ts similarity index 96% rename from src/migrations/Migration20250215152716.ts rename to src/migrations/Migration20250216202007.ts index 49e2473..3e3571b 100644 --- a/src/migrations/Migration20250215152716.ts +++ b/src/migrations/Migration20250216202007.ts @@ -1,14 +1,14 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250215152716 extends Migration { +export class Migration20250216202007 extends Migration { override async up(): Promise { - this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json 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;`); this.addSql(`create table \`map_effect\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`map_effect\` add index \`map_effect_map_id_index\`(\`map_id\`);`); - this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, \`teleport_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); + this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`teleport_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`map_event_tile\` add index \`map_event_tile_map_id_index\`(\`map_id\`);`); this.addSql(`alter table \`map_event_tile\` add unique \`map_event_tile_teleport_id_unique\`(\`teleport_id\`);`); From bfba7197b7fe55a97b98ee32a231094dd1da9fdf Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 21:22:49 +0100 Subject: [PATCH 18/43] #359: Throttle attack --- src/events/map/characterAttack.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/events/map/characterAttack.ts b/src/events/map/characterAttack.ts index 4ded236..8afbe7c 100644 --- a/src/events/map/characterAttack.ts +++ b/src/events/map/characterAttack.ts @@ -19,6 +19,11 @@ export default class CharacterMove extends BaseEvent { // Don't attack if the character is already moving if (this.getMapCharacter()?.isMoving) return + const throttleKey = `attack_${this.socket.characterId}` + if (this.isThrottled(throttleKey, 1000)) { + return + } + // Start attack await this.characterAttackService.attack(this.socket.characterId!) } catch (error) { From 4ac1e8824d4a8de8eb557641a238a2611a40eb64 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Mon, 17 Feb 2025 01:20:27 +0100 Subject: [PATCH 19/43] Socket event enum enhancement --- src/application/enums.ts | 6 ++++-- src/events/disconnect.ts | 2 +- src/managers/socketManager.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/application/enums.ts b/src/application/enums.ts index 563b4f9..9689bf9 100644 --- a/src/application/enums.ts +++ b/src/application/enums.ts @@ -1,4 +1,6 @@ export enum SocketEvent { + CONNECT_ERROR = 'connect_error', + RECONNECT_FAILED = 'reconnect_failed', CLOSE = '52', DATA = '51', CHARACTER_CONNECT = '50', @@ -35,7 +37,7 @@ export enum SocketEvent { GM_MAP_REQUEST = '19', GM_MAP_UPDATE = '18', MAP_CHARACTER_MOVEERROR = '17', - DISCONNECT = '16', + DISCONNECT = 'disconnect', USER_DISCONNECT = '15', LOGIN = '14', LOGGED_IN = '13', @@ -43,7 +45,7 @@ export enum SocketEvent { DATE = '11', FAILED = '10', COMPLETED = '9', - CONNECTION = '8', + CONNECTION = 'connection', WEATHER = '7', CHARACTER_DISCONNECT = '6', MAP_CHARACTER_ATTACK = '5', diff --git a/src/events/disconnect.ts b/src/events/disconnect.ts index f570ce3..3ba5205 100644 --- a/src/events/disconnect.ts +++ b/src/events/disconnect.ts @@ -4,7 +4,7 @@ import MapManager from '@/managers/mapManager' export default class DisconnectEvent extends BaseEvent { public listen(): void { - this.socket.on('disconnect', this.handleEvent.bind(this)) + this.socket.on(SocketEvent.DISCONNECT, this.handleEvent.bind(this)) } private async handleEvent(): Promise { diff --git a/src/managers/socketManager.ts b/src/managers/socketManager.ts index 3558dd9..0f202a7 100644 --- a/src/managers/socketManager.ts +++ b/src/managers/socketManager.ts @@ -6,6 +6,7 @@ import Storage from '@/application/storage' import type { TSocket, UUID } from '@/application/types' import { Authentication } from '@/middleware/authentication' import { Server as SocketServer } from 'socket.io' +import {SocketEvent} from "@/application/enums"; class SocketManager { private io: SocketServer | null = null @@ -21,7 +22,7 @@ class SocketManager { this.io.use(Authentication) // Set up connection handler - this.io.on('connection', this.handleConnection.bind(this)) + this.io.on(SocketEvent.CONNECTION, this.handleConnection.bind(this)) } /** From a77b35d55af679f4bc7c6810530e4f113e2c5eff Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Mon, 17 Feb 2025 02:15:12 +0100 Subject: [PATCH 20/43] Updated .env.example --- .env.example | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 68d322b..2721dec 100644 --- a/.env.example +++ b/.env.example @@ -6,10 +6,10 @@ JWT_SECRET="secret" CLIENT_URL="http://localhost:5173" # Database configuration -REDIS_URL="redis://@redis:6379/4" -DB_HOST="mariadb" -DB_USER="mariadb" -DB_PASS="mariadb" +REDIS_URL="redis://@127.0.0.1:6379/4" +DB_HOST="localhost" +DB_USER="root" +DB_PASS="" DB_PORT="3306" DB_NAME="game" From 4c7751db558e44e713f22ea4219c131608b2d3a8 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Mon, 17 Feb 2025 14:50:02 +0100 Subject: [PATCH 21/43] #363 : (Re)saving teleports works again --- package-lock.json | 6 +++--- src/entities/base/mapEventTile.ts | 2 +- src/entities/base/mapEventTileTeleport.ts | 2 +- src/migrations/.snapshot-game.json | 2 +- ...igration20250216202007.ts => Migration20250217134639.ts} | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/migrations/{Migration20250216202007.ts => Migration20250217134639.ts} (99%) diff --git a/package-lock.json b/package-lock.json index 91edb6f..683c634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4783,9 +4783,9 @@ } }, "node_modules/type-fest": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz", - "integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz", + "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" diff --git a/src/entities/base/mapEventTile.ts b/src/entities/base/mapEventTile.ts index 9b85382..4f75ffe 100644 --- a/src/entities/base/mapEventTile.ts +++ b/src/entities/base/mapEventTile.ts @@ -22,7 +22,7 @@ export class BaseMapEventTile extends BaseEntity { @Property() positionY: number = 0 - @OneToOne({ eager: true }) + @OneToOne({ eager: true, deleteRule: 'cascade', orphanRemoval: true }) teleport?: MapEventTileTeleport setId(id: UUID) { diff --git a/src/entities/base/mapEventTileTeleport.ts b/src/entities/base/mapEventTileTeleport.ts index d330459..f156d6c 100644 --- a/src/entities/base/mapEventTileTeleport.ts +++ b/src/entities/base/mapEventTileTeleport.ts @@ -9,7 +9,7 @@ export class BaseMapEventTileTeleport extends BaseEntity { @PrimaryKey() id = randomUUID() - @OneToOne({ deleteRule: 'cascade' }) + @OneToOne({ deleteRule: 'cascade', orphanRemoval: true }) mapEventTile!: MapEventTile @ManyToOne({ deleteRule: 'cascade', eager: true }) diff --git a/src/migrations/.snapshot-game.json b/src/migrations/.snapshot-game.json index 1ab2576..672ae91 100644 --- a/src/migrations/.snapshot-game.json +++ b/src/migrations/.snapshot-game.json @@ -317,7 +317,7 @@ "id" ], "referencedTableName": "map_event_tile_teleport", - "deleteRule": "set null", + "deleteRule": "cascade", "updateRule": "cascade" } }, diff --git a/src/migrations/Migration20250216202007.ts b/src/migrations/Migration20250217134639.ts similarity index 99% rename from src/migrations/Migration20250216202007.ts rename to src/migrations/Migration20250217134639.ts index 3e3571b..38a652e 100644 --- a/src/migrations/Migration20250216202007.ts +++ b/src/migrations/Migration20250217134639.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250216202007 extends Migration { +export class Migration20250217134639 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;`); @@ -70,7 +70,7 @@ export class Migration20250216202007 extends Migration { this.addSql(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); - this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_teleport_id_foreign\` foreign key (\`teleport_id\`) references \`map_event_tile_teleport\` (\`id\`) on update cascade on delete set null;`); + this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_teleport_id_foreign\` foreign key (\`teleport_id\`) references \`map_event_tile_teleport\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); From 0efa9fb1d5308a7d64f5fec5eee62b9ce393aa25 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 17:09:15 +0100 Subject: [PATCH 22/43] #245 : Enhanced asset CRUD logic --- package-lock.json | 6 +++--- src/application/base/baseEvent.ts | 2 +- src/entities/base/characterHair.ts | 14 +++++++++++++- src/entities/base/item.ts | 4 ++-- src/events/character/connect.ts | 4 ++-- .../assetManager/characterHair/create.ts | 12 +++++++++++- .../gameMaster/assetManager/item/create.ts | 5 ++++- src/events/gameMaster/mapEditor/update.ts | 2 +- src/migrations/.snapshot-game.json | 17 +++++++++++++---- ...0217134639.ts => Migration20250218160822.ts} | 10 +++++----- 10 files changed, 55 insertions(+), 21 deletions(-) rename src/migrations/{Migration20250217134639.ts => Migration20250218160822.ts} (96%) diff --git a/package-lock.json b/package-lock.json index 683c634..ed6e753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3316,9 +3316,9 @@ "license": "MIT" }, "node_modules/long": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz", - "integrity": "sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", "license": "Apache-2.0" }, "node_modules/lru-cache": { diff --git a/src/application/base/baseEvent.ts b/src/application/base/baseEvent.ts index 986b149..dc0efaa 100644 --- a/src/application/base/baseEvent.ts +++ b/src/application/base/baseEvent.ts @@ -45,7 +45,7 @@ export abstract class BaseEvent { return character?.getRole() === 'gm' } - protected emitError(message: string): void { + protected sendNotificationAndLog(message: string): void { this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message }) this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`) } diff --git a/src/entities/base/characterHair.ts b/src/entities/base/characterHair.ts index b257cae..51f801f 100644 --- a/src/entities/base/characterHair.ts +++ b/src/entities/base/characterHair.ts @@ -16,11 +16,14 @@ export class BaseCharacterHair extends BaseEntity { @Property() gender: CharacterGender = CharacterGender.MALE + @Property() + color: string = '#000000' + @Property() isSelectable = false @ManyToOne() - sprite?: Sprite + sprite!: Sprite @Property() createdAt = new Date() @@ -55,6 +58,15 @@ export class BaseCharacterHair extends BaseEntity { return this.gender } + setColor(color: string) { + this.color = color + return this + } + + getColor() { + return this.color + } + setIsSelectable(isSelectable: boolean) { this.isSelectable = isSelectable return this diff --git a/src/entities/base/item.ts b/src/entities/base/item.ts index 8e837b7..01b2808 100644 --- a/src/entities/base/item.ts +++ b/src/entities/base/item.ts @@ -25,8 +25,8 @@ export class BaseItem extends BaseEntity { @Enum(() => ItemRarity) rarity: ItemRarity = ItemRarity.COMMON - @ManyToOne(() => Sprite) - sprite?: Sprite + @ManyToOne() + sprite!: Sprite @Property() createdAt = new Date() diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index 49c984e..9ebabff 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -22,14 +22,14 @@ export default class CharacterConnectEvent extends BaseEvent { private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise { try { if (await this.checkForActiveCharacters()) { - this.emitError('You are already connected to another character') + this.sendNotificationAndLog('You are already connected to another character') return } const character = await this.characterRepository.getByUserAndId(this.socket.userId, data.characterId) if (!character) { - this.emitError('Character not found or does not belong to this user') + this.sendNotificationAndLog('Character not found or does not belong to this user') return } diff --git a/src/events/gameMaster/assetManager/characterHair/create.ts b/src/events/gameMaster/assetManager/characterHair/create.ts index 6952ea4..6d186a8 100644 --- a/src/events/gameMaster/assetManager/characterHair/create.ts +++ b/src/events/gameMaster/assetManager/characterHair/create.ts @@ -1,6 +1,7 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import { CharacterHair } from '@/entities/characterHair' +import SpriteRepository from "@/repositories/spriteRepository"; export default class CharacterHairCreateEvent extends BaseEvent { public listen(): void { @@ -11,8 +12,17 @@ export default class CharacterHairCreateEvent extends BaseEvent { try { if (!(await this.isCharacterGM())) return + // Get first sprite + const spriteRepository = new SpriteRepository() + const firstSprite = await spriteRepository.getFirst() + + if (!firstSprite) { + this.sendNotificationAndLog('No sprites found') + return callback(false) + } + const newCharacterHair = new CharacterHair() - await newCharacterHair.setName('New hair').save() + await newCharacterHair.setName('New hair').setSprite(firstSprite).save() return callback(true) } catch (error) { diff --git a/src/events/gameMaster/assetManager/item/create.ts b/src/events/gameMaster/assetManager/item/create.ts index 3bad4d5..cd7cd83 100644 --- a/src/events/gameMaster/assetManager/item/create.ts +++ b/src/events/gameMaster/assetManager/item/create.ts @@ -14,7 +14,10 @@ export default class ItemCreateEvent extends BaseEvent { const spriteRepository = new SpriteRepository() const sprite = await spriteRepository.getFirst() - if (!sprite) return callback(false) + if (!sprite) { + this.sendNotificationAndLog('No sprites found') + return callback(false) + } const newItem = new Item() await newItem.setName('New Item').setItemType(ItemType.WEAPON).setStackable(false).setRarity(ItemRarity.COMMON).setSprite(sprite).save() diff --git a/src/events/gameMaster/mapEditor/update.ts b/src/events/gameMaster/mapEditor/update.ts index 8989745..72c99b2 100644 --- a/src/events/gameMaster/mapEditor/update.ts +++ b/src/events/gameMaster/mapEditor/update.ts @@ -130,7 +130,7 @@ export default class MapUpdateEvent extends BaseEvent { return callback(await map.mapEditorObject()) } catch (error: any) { - this.emitError(`gm:map:update error: ${error instanceof Error ? error.message + error.stack : String(error)}`) + this.sendNotificationAndLog(`gm:map:update error: ${error instanceof Error ? error.message + error.stack : String(error)}`) return callback(null) } } diff --git a/src/migrations/.snapshot-game.json b/src/migrations/.snapshot-game.json index 672ae91..498b9b4 100644 --- a/src/migrations/.snapshot-game.json +++ b/src/migrations/.snapshot-game.json @@ -863,7 +863,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "length": 255, "mappedType": "string" }, @@ -923,7 +923,6 @@ "id" ], "referencedTableName": "sprite", - "deleteRule": "set null", "updateRule": "cascade" } }, @@ -1096,6 +1095,17 @@ "default": "'MALE'", "mappedType": "string" }, + "color": { + "name": "color", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 255, + "default": "'#000000'", + "mappedType": "string" + }, "is_selectable": { "name": "is_selectable", "type": "tinyint(1)", @@ -1113,7 +1123,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "length": 255, "mappedType": "string" }, @@ -1173,7 +1183,6 @@ "id" ], "referencedTableName": "sprite", - "deleteRule": "set null", "updateRule": "cascade" } }, diff --git a/src/migrations/Migration20250217134639.ts b/src/migrations/Migration20250218160822.ts similarity index 96% rename from src/migrations/Migration20250217134639.ts rename to src/migrations/Migration20250218160822.ts index 38a652e..daec3a8 100644 --- a/src/migrations/Migration20250217134639.ts +++ b/src/migrations/Migration20250218160822.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250217134639 extends Migration { +export class Migration20250218160822 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;`); @@ -24,13 +24,13 @@ export class Migration20250217134639 extends Migration { this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` 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) 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(`create table \`character_type\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` enum('MALE', 'FEMALE') not null, \`race\` enum('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') not null, \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`); - this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); + this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`color\` varchar(255) not null default '#000000', \`is_selectable\` tinyint(1) not null default false, \`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 \`character_hair\` add index \`character_hair_sprite_id_index\`(\`sprite_id\`);`); this.addSql(`create table \`sprite_action\` (\`id\` varchar(255) not null, \`sprite_id\` varchar(255) not null, \`action\` varchar(255) not null, \`sprites\` json null, \`origin_x\` numeric(5,2) not null default 0, \`origin_y\` numeric(5,2) not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`frame_rate\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); @@ -78,11 +78,11 @@ export class Migration20250217134639 extends Migration { this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`); - this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`); + this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade;`); this.addSql(`alter table \`character_type\` add constraint \`character_type_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`); - this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`); + this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade;`); this.addSql(`alter table \`sprite_action\` add constraint \`sprite_action_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete cascade;`); From 258ebf97d10cd48341a3cfbf68ebc541782adb65 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 17:52:25 +0100 Subject: [PATCH 23/43] Copy sprite fix --- .../gameMaster/assetManager/sprite/copy.ts | 17 ++++++++++++++++- src/managers/socketManager.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/copy.ts b/src/events/gameMaster/assetManager/sprite/copy.ts index 128a083..c38d8fb 100644 --- a/src/events/gameMaster/assetManager/sprite/copy.ts +++ b/src/events/gameMaster/assetManager/sprite/copy.ts @@ -2,6 +2,7 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { UUID } from '@/application/types' import { Sprite } from '@/entities/sprite' +import { SpriteAction } from '@/entities/spriteAction' import SpriteRepository from '@/repositories/spriteRepository' interface CopyPayload { @@ -29,7 +30,21 @@ export default class SpriteCopyEvent extends BaseEvent { await spriteRepository.getEntityManager().populate(sourceSprite, ['spriteActions']) const newSprite = new Sprite() - await newSprite.setName(`${sourceSprite.getName()} (Copy)`).setSpriteActions(sourceSprite.getSpriteActions()).save() + await newSprite.setName(`${sourceSprite.getName()} (Copy)`).save() + + for (const spriteAction of sourceSprite.getSpriteActions()) { + const newSpriteAction = new SpriteAction() + await newSpriteAction + .setSprite(newSprite) + .setAction(spriteAction.getAction()) + .setSprites(spriteAction.getSprites() ?? []) + .setOriginX(spriteAction.getOriginX()) + .setOriginY(spriteAction.getOriginY()) + .setFrameWidth(spriteAction.getFrameWidth()) + .setFrameHeight(spriteAction.getFrameHeight()) + .setFrameRate(spriteAction.getFrameRate()) + .save() + } return callback(true) } catch (error) { diff --git a/src/managers/socketManager.ts b/src/managers/socketManager.ts index 0f202a7..9507678 100644 --- a/src/managers/socketManager.ts +++ b/src/managers/socketManager.ts @@ -1,12 +1,12 @@ import fs from 'fs' import { Server as HTTPServer } from 'http' import { pathToFileURL } from 'url' +import { SocketEvent } from '@/application/enums' import Logger, { LoggerType } from '@/application/logger' import Storage from '@/application/storage' import type { TSocket, UUID } from '@/application/types' import { Authentication } from '@/middleware/authentication' import { Server as SocketServer } from 'socket.io' -import {SocketEvent} from "@/application/enums"; class SocketManager { private io: SocketServer | null = null From 66fc6d8b43d8dda29519e31d3cd3c314d08b8774 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 17:52:50 +0100 Subject: [PATCH 24/43] #245 : Added color field to character hair --- src/entities/base/sprite.ts | 2 +- src/events/gameMaster/assetManager/characterHair/create.ts | 6 +++--- src/events/gameMaster/assetManager/characterHair/update.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/entities/base/sprite.ts b/src/entities/base/sprite.ts index 2ef9e54..91859ad 100644 --- a/src/entities/base/sprite.ts +++ b/src/entities/base/sprite.ts @@ -11,7 +11,7 @@ export class BaseSprite extends BaseEntity { @Property() name!: string - @OneToMany(() => SpriteAction, (action) => action.sprite) + @OneToMany({ mappedBy: 'sprite', orphanRemoval: true }) spriteActions = new Collection(this) @Property() diff --git a/src/events/gameMaster/assetManager/characterHair/create.ts b/src/events/gameMaster/assetManager/characterHair/create.ts index 6d186a8..66977a2 100644 --- a/src/events/gameMaster/assetManager/characterHair/create.ts +++ b/src/events/gameMaster/assetManager/characterHair/create.ts @@ -1,7 +1,7 @@ import { BaseEvent } from '@/application/base/baseEvent' -import { SocketEvent } from '@/application/enums' +import { CharacterGender, SocketEvent } from '@/application/enums' import { CharacterHair } from '@/entities/characterHair' -import SpriteRepository from "@/repositories/spriteRepository"; +import SpriteRepository from '@/repositories/spriteRepository' export default class CharacterHairCreateEvent extends BaseEvent { public listen(): void { @@ -22,7 +22,7 @@ export default class CharacterHairCreateEvent extends BaseEvent { } const newCharacterHair = new CharacterHair() - await newCharacterHair.setName('New hair').setSprite(firstSprite).save() + await newCharacterHair.setName('New hair').setGender(CharacterGender.MALE).setSprite(firstSprite).save() return callback(true) } catch (error) { diff --git a/src/events/gameMaster/assetManager/characterHair/update.ts b/src/events/gameMaster/assetManager/characterHair/update.ts index 8b03457..b2711d4 100644 --- a/src/events/gameMaster/assetManager/characterHair/update.ts +++ b/src/events/gameMaster/assetManager/characterHair/update.ts @@ -8,6 +8,7 @@ type Payload = { id: UUID name: string gender: CharacterGender + color: string isSelectable: boolean spriteId: UUID } @@ -29,7 +30,7 @@ export default class CharacterHairUpdateEvent extends BaseEvent { const characterHair = await characterHairRepository.getById(data.id) if (!characterHair) return callback(false) - await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save() + await characterHair.setName(data.name).setGender(data.gender).setColor(data.color).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save() return callback(true) } catch (error) { From 78daac9d9503902c31fad05b710deaeb7318885d Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 18:00:20 +0100 Subject: [PATCH 25/43] Remove hair works again --- src/events/character/connect.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index 9ebabff..a3f54fb 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -37,7 +37,7 @@ export default class CharacterConnectEvent extends BaseEvent { this.socket.characterId = character.id // Set character hair - if (data.characterHairId !== undefined && data.characterHairId !== null) { + if (data.characterHairId !== undefined) { const characterHair = await this.characterHairRepository.getById(data.characterHairId) await character.setCharacterHair(characterHair).save() } @@ -62,7 +62,7 @@ export default class CharacterConnectEvent extends BaseEvent { } private async checkForActiveCharacters(): Promise { - const characters = await this.characterRepository.getByUserId(this.socket.userId) + const characters = await this.characterRepository.getByUserId(this.socket.userId!) return characters?.some((char) => MapManager.getCharacterById(char.id)) ?? false } } From c14ae36a94cde8756d4f6e30135b223f958af647 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 18:04:02 +0100 Subject: [PATCH 26/43] ! --- src/events/character/connect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index a3f54fb..d9059dc 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -26,7 +26,7 @@ export default class CharacterConnectEvent extends BaseEvent { return } - const character = await this.characterRepository.getByUserAndId(this.socket.userId, data.characterId) + const character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId) if (!character) { this.sendNotificationAndLog('Character not found or does not belong to this user') From b7dd0cbd7571a698ff3f6f8092ed4c857f011fcd Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 18 Feb 2025 21:29:37 +0100 Subject: [PATCH 27/43] Added default hair color to init command, set updatedAt when saving sprites --- src/commands/init.ts | 2 +- src/events/gameMaster/assetManager/sprite/update.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index f20212a..3c87169 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -326,7 +326,7 @@ export default class InitCommand extends BaseCommand { .save() const characterHair = new CharacterHair() - await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setIsSelectable(true).setSprite(hairSprite).save() + await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setColor('#1B1212').setIsSelectable(true).setSprite(hairSprite).save() } private async createMap(): Promise { diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 7bac5cf..2e64f0f 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -56,7 +56,7 @@ export default class SpriteUpdateEvent extends BaseEvent { await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) // Update sprite in database - await sprite.setName(data.name).save() + await sprite.setName(data.name).setUpdatedAt(new Date()).save() // First verify all sprite sheets can be generated for (const actionData of data.spriteActions) { From 2cbc951816a5ccdc3210c8685ecb4d05fd724e7d Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 19 Feb 2025 11:22:15 +0100 Subject: [PATCH 28/43] init command enhancement --- package-lock.json | 230 +++++++++++++++++++++++-------------------- src/commands/init.ts | 4 +- 2 files changed, 127 insertions(+), 107 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed6e753..b53c4bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -466,9 +466,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -482,10 +482,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -500,9 +517,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -517,9 +534,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -534,9 +551,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -551,9 +568,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -568,9 +585,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -585,9 +602,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", "cpu": [ "x64" ], @@ -1932,9 +1949,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz", - "integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==", + "version": "5.41.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.3.tgz", + "integrity": "sha512-tWTeuO/BHDg6gKVnQJMjO42zkhsGss6s4bMdgJU24JVBT53yUvDjaO9H0L/BHKAtsMi4xlxkrDuMNSYWeHlekA==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -2421,9 +2438,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2434,30 +2451,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -4763,13 +4781,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { diff --git a/src/commands/init.ts b/src/commands/init.ts index 3c87169..db2d9a1 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -345,6 +345,8 @@ export default class InitCommand extends BaseCommand { private async createUser(): Promise { const user = new User() await user.setId('6f9a58b4-172d-425e-b9ea-71e1d13d81ee').setUsername('root').setEmail('local@host').setPassword('password').setOnline(false).save() + const map = await this.mapRepository.getFirst() + if (!map) return const character = new Character() await character @@ -352,7 +354,7 @@ export default class InitCommand extends BaseCommand { .setUser(user) .setName('root') .setRole('gm') - .setMap(await this.mapRepository.getFirst()) + .setMap(map) .setCharacterType(await this.characterTypeRepository.getFirst()) .setCharacterHair(await this.characterHairRepository.getFirst()) .save() From 39d793570d22812c0f23428116bced94555bbdb6 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 19 Feb 2025 11:45:43 +0100 Subject: [PATCH 29/43] #244: Allow nickname changes --- src/application/base/baseEvent.ts | 5 +++-- src/application/zodTypes.ts | 13 +++++++++++++ src/events/character/connect.ts | 20 +++++++++++++++++++- src/events/character/create.ts | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/application/base/baseEvent.ts b/src/application/base/baseEvent.ts index dc0efaa..e309089 100644 --- a/src/application/base/baseEvent.ts +++ b/src/application/base/baseEvent.ts @@ -46,14 +46,15 @@ export abstract class BaseEvent { } protected sendNotificationAndLog(message: string): void { + console.log(message) this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message }) - this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`) + this.logger.error('Base event error' + `Player ${this.socket.userId}: ${message}`) } protected handleError(context: string, error: unknown): void { console.log(error) const errorMessage = error instanceof Error ? error.message : error && typeof error === 'object' && 'toString' in error ? error.toString() : String(error) this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message: `Server error occured. Please contact the server administrator.` }) - this.logger.error('Base event error', errorMessage) + this.logger.error('Base event error: ' + errorMessage) } } diff --git a/src/application/zodTypes.ts b/src/application/zodTypes.ts index 7252173..a2010ab 100644 --- a/src/application/zodTypes.ts +++ b/src/application/zodTypes.ts @@ -58,3 +58,16 @@ export const ZCharacterCreate = z.object({ .max(255, { message: 'Name must be at most 255 characters long' }) .regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }) }) + +export const ZCharacterConnect = z.object({ + characterId: z.string(), + characterHairId: z.string().optional(), + newNickname: z + .string() + .min(3, { message: 'Name must be at least 3 characters long' }) + .max(255, { message: 'Name must be at most 255 characters long' }) + .regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { + message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' + }) + .optional() +}) diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index d9059dc..95d777d 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -1,6 +1,7 @@ import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { UUID } from '@/application/types' +import { ZCharacterConnect } from '@/application/zodTypes' import MapManager from '@/managers/mapManager' import CharacterHairRepository from '@/repositories/characterHairRepository' import CharacterRepository from '@/repositories/characterRepository' @@ -9,6 +10,7 @@ import TeleportService from '@/services/characterTeleportService' interface CharacterConnectPayload { characterId: UUID characterHairId?: UUID + newNickname?: string } export default class CharacterConnectEvent extends BaseEvent { @@ -21,18 +23,34 @@ export default class CharacterConnectEvent extends BaseEvent { private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise { try { + const result = ZCharacterConnect.safeParse(data) + if (!result.success) { + this.sendNotificationAndLog(result.error?.errors[0]?.message ?? 'Invalid data') + return + } + if (await this.checkForActiveCharacters()) { this.sendNotificationAndLog('You are already connected to another character') return } - const character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId) + let character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId) if (!character) { this.sendNotificationAndLog('Character not found or does not belong to this user') return } + if (data.newNickname) { + const existingCharacter = await this.characterRepository.getByName(data.newNickname) + if (existingCharacter) { + this.sendNotificationAndLog('Nickname already in use: ' + data.newNickname) + return + } + + await character.setName(data.newNickname).save() + } + // Set character id this.socket.characterId = character.id diff --git a/src/events/character/create.ts b/src/events/character/create.ts index e4e9a42..1895c37 100644 --- a/src/events/character/create.ts +++ b/src/events/character/create.ts @@ -32,7 +32,7 @@ export default class CharacterCreateEvent extends BaseEvent { } private async createCharacter(data: z.infer): Promise { - const user = await this.userRepository.getById(this.socket.userId) + const user = await this.userRepository.getById(this.socket.userId!) if (!user) { throw new Error('You are not logged in') } From b673e7a1760c6304f9a05c579dd0fba5cf093eed Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 19 Feb 2025 11:51:18 +0100 Subject: [PATCH 30/43] empty = undefined --- src/events/character/connect.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index 95d777d..67256dd 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -23,6 +23,7 @@ export default class CharacterConnectEvent extends BaseEvent { private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise { try { + if (data.newNickname === '') data.newNickname = undefined const result = ZCharacterConnect.safeParse(data) if (!result.success) { this.sendNotificationAndLog(result.error?.errors[0]?.message ?? 'Invalid data') From d6681f9af74967e4d19ec69077e2a17a37c6a59f Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Thu, 20 Feb 2025 00:44:36 +0100 Subject: [PATCH 31/43] Added width and height fields, init fix --- src/commands/init.ts | 215 +++++++++++++++++- src/entities/base/sprite.ts | 24 ++ src/migrations/.snapshot-game.json | 22 ++ ...18160822.ts => Migration20250219234315.ts} | 4 +- 4 files changed, 259 insertions(+), 6 deletions(-) rename src/migrations/{Migration20250218160822.ts => Migration20250219234315.ts} (98%) diff --git a/src/commands/init.ts b/src/commands/init.ts index db2d9a1..62757b4 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -30,7 +30,8 @@ export default class InitCommand extends BaseCommand { // Assets await this.importTiles() await this.importMapObjects() - await this.createCharacterType() + await this.createMaleCharacterType() + // await this.createFemaleCharacterType() await this.createCharacterHair() // await this.createCharacterEquipment() @@ -74,9 +75,9 @@ export default class InitCommand extends BaseCommand { } } - private async createCharacterType(): Promise { + private async createMaleCharacterType(): Promise { const characterSprite = new Sprite() - characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Character') + characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character') await characterSprite.save() const idleRightDownAction = new SpriteAction() @@ -272,7 +273,213 @@ export default class InitCommand extends BaseCommand { const characterType = new CharacterType() await characterType .setId('75b70c78-17f0-44c0-a4fa-15043cb95be0') - .setName('New character type') + .setName('Male character') + .setGender(CharacterGender.MALE) + .setRace(CharacterRace.HUMAN) + .setIsSelectable(true) + .setSprite(characterSprite) + .save() + } + + private async createFemaleCharacterType(): Promise { + const characterSprite = new Sprite() + characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character') + await characterSprite.save() + + const idleRightDownAction = new SpriteAction() + await idleRightDownAction + .setAction('idle_right_down') + .setSprites([ + { + url: 'data:image/png;base64,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', + offset: { + x: 0, + y: 0 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(28) + .setFrameHeight(94) + .setFrameRate(0) + .setSprite(characterSprite) + .save() + + const idleLeftUpAction = new SpriteAction() + await idleLeftUpAction + .setAction('idle_left_up') + .setSprites([ + { + url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAABdCAYAAAC7F3YaAAAF6UlEQVR4nNWaz0sjSRTHv4lDlEQREklmQGhh0uRiQE8yCF6CHvMXyOJx2aOelgUPgT0q7HFvgRUWr2FOE7x486SQuWSSARtknG60WYmGmYBmD50qq6qrqqsT5zDvlNSvT72uV/Veve5E5fe/YSLZRm0oK/er+wmT/q9MAbtba7DKJa7u/OQU9VF9FFALyjZqw92tNdiVLcy+fc3V3X/+itXKBrL5AnzPRb1RG+pgSlC2URv+tfcLZuxlpBcymMrZXP0sgHsAVhmwEGiqgyV1mqTylhRCO8/PIZW3gj75gmrOahAA2JUtJOfntJ1Zscol7KwsKY0mBCLa6OTxtoP+zUMwgOFkpGtklUt0gP7NA9LocPUEEkcizTtq4Ke7HgDAabW1YyjXiAxgKr7n4t2vv8UDRc1OnMz5yakWogSJA+nqzv79h0K+dT4q20vXyPdcIxjA7x/dk1BqNPAcLYC0Iefft85HboKihDTyq/uJeqM2zOYLsMp8XSpvhSbgtNqwMIHV+Z4b2ZnUO622VhslyK/uJ+oXlyEYewqQct9zIyFKEAtjB5VBWKlfXCr9kta8/ep+4vDDWeRs6xeXWkgkKI5EedhYoLjHEisJWXDC+hTiMsR4gcj5ySmA4PEBas1CoGyjNtxZWaI73iqXQvuHeFVSxpq3CshtWAJZrWxwgybn55BCACP/0wsZ3I/asHGDKlgJnQzs2cUO2gcALyhPL2QABPvq6a5HNSSRkixYocZAtCFrQToDwFTOpoOzZazMvn2NqZyN7Lu10IQ5EFspO3rYgWVR0f3nr7Ruxl4OBSsciLUsdvEfb/mYQSwbeA46Jx/o//RChnsiFEQeG5HDD2chrdiBZWDfc2n5VM5Gcn6Oe3ycRjP2cmgAE3Fabbz/9CUUxLBP6NkY8oXQgo8jRCtxLE4jVehrGsc93fWUbY3POhMYMaBe+0INUp1lqoNUBj4/OVW2D2kk20OqzqScnHPvP33BwHPoI2RNnAPJzFYm48TeSbKHZuxl9G8elP5f1CrKN2mt7kdKcDKMdrAY0qqCSJ02qqiJajTuZmUnc9MfKNslgcC0yWYlHjKuiBBx80eukUkMbiIUJJo2a30Dz6FAFjzwHG3YzC6HViPVICxYFN9zpXUUxG7C20IJB9fTkYF+VHDPrpPysry5uYmDZhO4/g9/IgwjgIPr6aCgUELOfW73eNtRg8TZb29vw3Vd/NFsSqYyTScEAM1Rm/rFJQ3XHm872FlZQr1RGyo1KhQKKBaLKBaLWF9fR7fbRbFYpPXdbpf+dt1AO7+6n2CjXHY56BqJpwI7qOo/KSsU+NDKabWDRMhChp46WqtbXFzE4uKitIyUixMgkl7IyDUC+L3DAshvGVQlRKMQyDSJYSpSjUwuu3HE99yQc0yS66N4PTw+PqaNrq6u0O12cXV1xXVm/7N7SCavgPBdJue20Ww+WxMxX5l0u11lPZvkUO6jvTffcXB0xJVtbm7Cdd3QBJrNJnISSEgjlRxtB1eQ85NT1C8uIT0gRpOqS5RyWm26HErQamWDXsRWERwte2++I0jdlLjBZIYklscKTrL5AuzKFmbsZczYy0jlLdiVLWlb33M549KCxMQsybWyV8sQYJRxERMcRjlVVkgePI3nTHFwnl1ysNCk44LGlbFApqHzxKBxJBI0Sf7HCMSe5i9xsms10qWXXxQE8AEjcdEAHw+YuJjY++jprhfkhRBv/YxAYnicgsX9N5FY5i0+IgKJyqfGBk0iE4NMTV8LEjNUk4gSJEsQRr2ZHAsEPPubSQCRIKKJ6NyIe38xkExYDxsXZgRig5Fx0wQ/zz76eUCydxE/BPTSonXlJv5GlSI1BgGgacson0NeDai+ZVCCyOXMabW1+R7264ydlSXthxNKD+tX9xOHjdoQkpeJNCQGkIIVvBhu6WMHrStnvSaZaf/mAXPirctAYlvd012P+xTEVGJFQU6rDasM+soNeM7ZifehsUFkzXYBgMl2EQBpo+ovfT2qE5lVmXy/9T+cHD3r8fXi8gAAAABJRU5ErkJggg==', + offset: { + x: 0, + y: 0 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(26) + .setFrameHeight(93) + .setFrameRate(0) + .setSprite(characterSprite) + .save() + + const walkRightDownAction = new SpriteAction() + await walkRightDownAction + .setAction('walk_right_down') + .setSprites([ + { + url: 'data:image/png;base64,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', + offset: { + x: 7, + y: 8 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 7, + y: 2 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 2, + y: 2 + } + }, + { + url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAABeCAYAAABLubY/AAAHiElEQVR4nNVaT0gbWRz+TCUVbRBGO1EojFDDXCLEy4oL24ureyke9LhSPG2lLAgVCnuoBy9lDyn0stveQtlL9+hRySUXERZiUVhCUkig0GbQoZIqVlD3kPxe3nszk5k3kxz2g4HM5M3MN7/3+/O9P31zv71BWGjbWzdu1+3Fzb6wz+yPQmQ1M4HpuQeO/9ezzf/DEFMmpG1v3axmJqDpSRhTJgZSaQyODglt3rxO46J8hPXs1o0qqVgYMtNzDwQyt0ZSwjE4OoSBVBqvNh55dmtXCAGApicBAAOptGebKKQCEyLrGFNm0FsAdCYeiZAXzo/PcHVS7thGxUqhosyN1CDKjmth0BVCUQjIUOqy2bUnqB2WAADXp43A961n3/aG0EX5CObSMor5Ai6tmvDf47VneLz2zHHP47VnKKQne0OIrDO79gTFfMHx0nfvqw5S795XVV6hRsi26rgoHwFo5iPqNnrp+M8/KhMITche3OzLHVSZlYwpE5dWDdenDXzZ+5u143/TefGn7wMTUo4yslLtsMRIxWHg319/Edrx9e2ic5oSoNZlnJVsq86uk4MPjg6xg6CaDpQztb242fdyZx8ABMe+Pm3g/PjMcVyfNrCefRtYioQuHbmDqkCK/Ek+5PTgh9CZejUzwUgR3MVacOtEIgQATxdmYFt1RkwmCKirxr6gmpqv1rxi9ELtsATyNRVivhbi9TMPIkOq8fz4jCXNgVQaZiqNV1Mmi8jcdjA529FCJMqAtlL0sgqfCtysV8wXkDuo+lrKk5As5rXZGQDA1w+fAQCx4YTjHj5h8kqRrgPAy539jqQ6hj1PhgR8bDjByAyODiFhZpAwMywZEhn++kAqzSz2dGGmo3p0JcTr54FUGrdGUo42NNoguLXhQZbz0+QdLRTXDaEMkHamrMxraVlXN0oHrlo7rhvNKPWwkmeUaXoSseEE+/Krk7JQl65PGzgHgOMDx70X5SMMpNKsPUVfXDdw5/5YK0Cqru91ENK2t26eLswgNbfArNMotV9K5SCuG54ytnZYgiGd21Ydw9/9AKAVqVyO4uHZZWQd3uwX5SOU8zsA4KhRdF7MF2BbdRTzBXZQguSf5dVtrl1G1qFuIqtQ6PJaiEgQKF8ReJli7+3j7sMU7j5cAbh7PAlRdFFYkw8QGduqsxfKJIwpE3Gd76jmfXa+SSh3UIWmJ6HNlnFrJOXpR76l4/q0IbxcJgE0nZXPTfQhcYgEbauOrx8+Y3gk5elHTqduWYDvKqrifD2TyfB5KTHSikqXDyzndzB9f8zTAMypqbvMpWX256VVY3qG5CsP3jJyYpTP6X7bqsPe2/fMR0KUaXqShTpff4QHc05KkCes/K7Tc+UAcBDi07os5IPi6qTMwlsW+LKVjSnTYSXBh+S5nCByQSbDSLQyuFs02VadJVcZronx+rShZB2VoY484JS7LQa0HZr6nCJLto6bY4cm1vpgufozC/EjBjdn9gLVM7nEdCTT+rByfgfa7Az+/P058yOhyyhU/brL73+VD5LTAyOkOjnJg/QRgXJZkEktsixFW9em9FRm1AhkaV4jMQt5JTEv1A5LysNkN9CggZw7pm1v3bzaeCQ0CpJ/bKvOSHkRu7Rqnv4mRyzlJOUuO0mayH5qnXz6Arzfx8b4NwCiAqBMn/10G0ia8Pq6S6sGcMU2lA/Nz88L59ndXYFgG7cxPz+PXfrfBcV8ATO6weRtaKdOJtsZdmVlxbNdve6f8S+tGq5OxrwJrWYmOo7FeTKTk5O4d+8eO//48SP7XalUhLYy7MXNvtz21o2mJ5kPOWpZW176QyYDQDifnAw2P03FViBEie2fF89DyQ4elUolcFuKNhKC/UAzIsxWpqYJqCjrpjyxID7EvytGJFQyLb2kUqkIPgO0fYjI7O7uYqQevLb1k2NNzzU93c86I/USKIrl8JeJAMDG+DfkFDyARVkxX8Bcaw7ID5QIsx3yy8b4N2h6Utkf+4F2+M0qCCxNT+Kvuba4orFb7qAqzEHSQDEohLDfe/1HYDI0UqVDnhJ2G8kqEWLjJtV19uEE7twfc7w8rhueUrgThEwdhgyNWO/AOcJQUY7smao38Gsdcqowl5bZDH9YrRR6rUP+elkbhx1odnXdfnB0SKiDoXwyLJGoIxMvRFqeoiUpstL58ZngR2HqofpmFG65gcAPg8KMPiIR4sE7Ni3YAe7TLD0lxL+QD2+39Y+eEpK7i3dcSpJRSUUOe54MbWSKAvU9aK3uokjS9CQMAIOjMyzSokjgUMMgY8oEDgG+btl7+6FqlwzlLV8CKQmq28EiEfID6aKopNSiTBq7Ox7WirCe5yG37Cw7LpEJoxKVCbmBtDPQXipPmJlIZCIR6hX+34Ro1oxHN0JdmRA//UbLl71C4ExNg8meMWlBqXTwClBe55IXjMOia07dDTJAl6OsG/thuzKTz3Y5dAGRLMTXrKjinhC5y/jaFWZXXtcJdRtdJRQbTrDNS34bl3pGiM0vt4R+wszg7sMVmEvLoUhFIsSvCBGi5qPQYU+lZDWDpuBv7ZhpIrzgD7yx0gt+XaI64fAfOTt3rh/rCvQAAAAASUVORK5CYII=', + offset: { + x: 0, + y: 0 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(36) + .setFrameHeight(102) + .setFrameRate(7) + .setSprite(characterSprite) + .save() + + const walkLeftUpAction = new SpriteAction() + await walkLeftUpAction + .setAction('walk_left_up') + .setSprites([ + { + url: 'data:image/png;base64,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', + offset: { + x: 3, + y: 2 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 0, + y: 2 + } + }, + { + url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABZCAYAAADVeL+8AAAFcElEQVR4nLVZzUtbWRT/JUqURAkkIWlBeIUmuDHQrqQI3Ygu/Qtk6LLMUldDqYtAF10ozKrMLjDC7LNsEMSdm7GQboIRfBDQ99BHxQ/aQM0sknM9971z33upmQOB5H78zu/ee+75uEks//EXoiTXqPWldm9tKxE2bzIO6MbqIqzqvNZ3tHeA+rDfpMQInmvU+huri6gsr2Lm+ROt7+bkHC+XXyNXLMFzHdQbtb6kQATPNWr9Pzd/w3RlAelCBhP5itY/A+AGgFUFLAxWJClImhinipYIrCZmZ5EqWoM5xZI8RmqsLK8imZ0VJ0hiVefx5sWzwMFr4MQ6TH5eHuPu4nYwOYJAYM+t6ryadHdxizSOtX4CjiOhphgFdn91DQCwW22xX9xzmhRXPNfBq7e/R4ObWJgIHO0diMAiuH9yWN/hP38r4O/HXwPjAnvuuU4sBYBu39KKReY91w4FpTHkb74ff9VIkWjMvbWtRL1R6+eKJVhVfWCqaAWU2q02LANrI3PPdSIPlvrtVltkLYJ7a1uJ+pfTgAJ+G6ndcx0jsJn5UAEHkoC51L+cBvy60RS9ta3EzudDIysOKgGHgo8ipkgUG3xUlwAACX+A5j6Z3K8/fpIc7R0AkPc7AJ5r1PpvXjxTN8+qzgfsm6IPtZEpkgFwJZMSMDFNFa1BOMNAAf1OFzK4Gc6jOCoFa+2GSsDpQgZ3AOAOxqQLGQADu7+/ulYrkYJ1krPmwCQT+YoC5G1cZp4/Qe7VoiJIoqyFGqVrz8Ek4Il8BRP5CqYrC1qwVuDcIvgB/rzUY6i/7ebkXP1OFzLaqjU7t1tt7Hw+DLDnYH5lPdfGzcm5WlUyO6t2QYFzjaMIEZFWOJbr7xfa4kjwuHnK3cVtgH0s5mEKRvbnJCZnZVLmb49kblLA200BfeQDlVhHBuieaxv3z88+rm8fmymSQu6HjOCmfZRYe66DnmurSoQu5KOYR2VmY9kWikIjgcfJGcPEmM5xBaSEK+u5dmTKZ2RumsiVRUkA/LI0j+2zqchkNCwBJRELrpWVFWw3m8DZN3xAUAGBbp9NAQDyQ2UUR2llxmpufX0djuPgXbMp9E4pEgDw7+4DAe52RfBSqYRyuYxyuYylpSUAQKfTEUk4jnlrRPByuSwO9rd3Oh2USnLdD7AD9R/e3Nyc+pgUmkgQlmiKBOj/7VcYJQo8yqyiCJBwf58EHsoUk48g6Xa7xj7Pdcxhjqe++/v7AVCylm63qz4mCyIJWMvm0x/Y3t0F8HBgBCKB+U2RF7xifv6hCLwbKiChC0OmR6DNZhN5BmxkzvPz3apeNYgXdbjSOiNvt9qqjNHA+dvW/dX1oERvAcApNp/+UAQAPSvmNSu3Os3OqZKg78CDiXJL4kUCT2BplWQcGjgFWH8lAehlOwEns7NIZmfx6eN7VezyV7rINy7OxP+qMZ1dUKv89PE9i06nD9tClsJLElPiQ2W73Wor6+BjxbKFREriRSXD7SFgKiUBBCsLXoXRNTaFOdp/u9VWMZUXw6RYYx6XtZ89MfcTCmwLdz6jekoSMgINnIDjZrH+1+fQS3R/da0dUFwxVYKhT3+mpw5ezoeVmGIF/RgxXn8uUXmgsuWQN/SxpNB+JxcA/5Xy3D8nVyxpjivgcklGtXGJ3CSdPAGTZzNZCpeeayMF/c1LA6cv6UJmpP8jPNcZRqm23uYH99+0qC0hv05+W+pX4L9i41FbBvxP7y2h4Ka/w8YCPi4RwcflZ4zMpT/yHgWeLmQwXVlAqmjh5fJrbKwuPkqBAh/lAsUVdUPvr64HD8LjBrdbbVhVaH4iToUcCe6tbSV2GrX+BgDyE6bH9lFFvfz7D+4xoCT/AaNCPNJBtKgMAAAAAElFTkSuQmCC', + offset: { + x: 5, + y: 6 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 2, + y: 6 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(34) + .setFrameHeight(101) + .setFrameRate(7) + .setSprite(characterSprite) + .save() + + const attackRightDownAction = new SpriteAction() + await attackRightDownAction + .setAction('attack_right_down') + .setSprites([ + { + url: 'data:image/png;base64,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', + offset: { + x: 20, + y: 0 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 19, + y: 8 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 17, + y: 3 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(69) + .setFrameHeight(111) + .setFrameRate(5) + .setSprite(characterSprite) + .save() + + const attackLeftUpAction = new SpriteAction() + await attackLeftUpAction + .setAction('attack_left_up') + .setSprites([ + { + url: 'data:image/png;base64,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', + offset: { + x: 2, + y: 0 + } + }, + { + url: 'data:image/png;base64,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', + offset: { + x: 5, + y: 0 + } + }, + { + url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABcCAYAAACFtS4PAAAFtUlEQVR4nKVaPWgbSRT+ZB9KsGMcZLHKcYY1xELFRcSpQpo0wi7dXXFgDpd3VzpVOHAhcBnDcVU6QwLXi6ss3BhcuLJBbnRywAvmkl1kcca28AkiXbF+4/l5MxrZXxNrd+Z73868efPeTDKVt+/hQq5WHXDPO8vrGWdHAN8MI11beomwXFLeHezsYuvmvcsIS56rVQdrSy9RrCzh0dMnyrvLT1/wovIauaCAThJjq1Yd2AwY5LladfD7m5/wsPgME/lJjM8UlfePAFwCCMtAiPSLbAbGOMXZIGSJRafpKWSDMO0TFNg2BjkAFCtLGJuesnbQEZZLWF2YYydekJNqF76etdBtX6UdPQQoYx6WS6JTt32FCbSUxkTsC6srDiPrn18AAKJG09rGGHPq5ItOEuPVz78OJ3ep4AQc7OxaiQ1yvbPr3f6fHwTxdeuIbauMeSeJvQwAqn/bvthQ3ksiJym1oXhz3TpSRMkQyjvL65mtWnWQCwoIy2qjbBAaRqNGE6FDNau8k8RDJ5beR42mVbVB3llez2wdnhgG5NVIzztJ7CTmld8YkIk4YhlbhydsXGddsbO8ntnc3neqIlIbsZVch8stXTtRhvZQPWRShNS3OMLBzi6AVL3NSKby9j1ytepgdWEuNXKzOMJyyXBB2iDomewtnBFlherEY9NTyCI1QL8n8pO4vGkvb3Xcnpr54fvvBqsLc0gXT0koJKJu+wrXrSNkg1Bs1t32FTsPvSRKM4ObSRYTSmqBW78enyliIj+pEBgb9tMnwmg2CJWYM0afRMQ6ZDJuwx6fKWJ8pigMFCtLYk8VyuVVSJ/89awFHdwzMkIGSL0g759foJdExqqUyXTiXhJZjSnkgP9OJOPy0xc7OS31XhINDUS+IB7n8h+WShzs7LKbi+GKAPDX3/94G/DZsazKbcHKZoybWEHOTabNQP/8QmlPxN32lfJFXiGXIKvWJ59CAkVLL3JdvfybVjYR95JI2TwE+V3cUI/1+tCyym2ewM0BhQ1unYwB6qbsC3JbPWoa5KNC/jJbaWOQt7s9lkCHLTw7yQF1YntJJIzIxnpJpAgh6GnGrbdID23RUTYmQ19UrPKzQgnvPj8Ymi/6hmajJlpcXMS7eh34/C82YJLY1gP3nC24VlZWEMcxfqvXmbcPAAAzTs0W8kKhgPn5eWEEgPgt449ffhxKbngLEen/2uDaQ511KBHPzs4qz09PT1WFlmqaXaEyGf2dz+eRz+eVZ1GjmVba+Un2gMG6/HW1NlDKx8E6LPqnt9ttth0p52Ao39vbE8THx8eGEd2wt/KZuIl6PXVHAIhj9wbSSWJn+mEMy5tv/8O7jx+VZ4uLiwD8jVrJAWDj+eM0kT88werCXBoONMgr1FZFs+RhuYTOzm3jjeePlf0yajSxuX1LPJJygNLgE+SCgnLu1T+/SMt3qZSMGk22ZGQraADY3N7H6sKcOJoid9NXo6tEZxcRxWvKS+gYkPNnmhuuVGSHRVZiixurC3POGtRKTgjLJePwknIXV1kuhBmqb8adil6CfKbou80585ZsELJ5iW/qZyXPBQVRncmqKdm8FzkHWwphw9AT0Qm0nHn5ncn75xfoSn+PCie5TupTZMmwjrme2BMx56Yjk3OQw8K9yOU0mVSPWtoY5PKRlAwKqxQpbVcJTnIdtkn0GRqWnOtIQ0Jh2Ec9S2477vMRYSWn8X5YfGac3+ou6KOeVe4q/2RkgxAvKq+H3xMJ9Y7xHtWAQR6WS87aEjArOxtGWqFyHJcrO1sYFoGLbhNzr9QrNDknydWqg04SAw2VZOidhQ/oXgM4sb53kuvjHZZLSnblcz1M8BpznzhyZ/JcULiTAYOcSr+J/ORIl65e5KPeeY5E3j+/UPKU+0DxlqjRRFiGuDagU+m7nvEqd3ObtepgDQCk0wr5wmMUNwSkqxzCff77g47/AX/tU4BIJmZXAAAAAElFTkSuQmCC', + offset: { + x: 6, + y: 1 + } + } + ]) + .setOriginX(0) + .setOriginY(0) + .setFrameWidth(34) + .setFrameHeight(100) + .setFrameRate(5) + .setSprite(characterSprite) + .save() + + const characterType = new CharacterType() + await characterType + .setId('75b70c78-17f0-44c0-a4fa-15043cb95be0') + .setName('Male character') .setGender(CharacterGender.MALE) .setRace(CharacterRace.HUMAN) .setIsSelectable(true) diff --git a/src/entities/base/sprite.ts b/src/entities/base/sprite.ts index 91859ad..f490d20 100644 --- a/src/entities/base/sprite.ts +++ b/src/entities/base/sprite.ts @@ -14,6 +14,12 @@ export class BaseSprite extends BaseEntity { @OneToMany({ mappedBy: 'sprite', orphanRemoval: true }) spriteActions = new Collection(this) + @Property() + width: number = 0 + + @Property() + height: number = 0 + @Property() createdAt = new Date() @@ -47,6 +53,24 @@ export class BaseSprite extends BaseEntity { return this.spriteActions } + setWidth(width: number) { + this.width = width + return this + } + + getWidth() { + return this.width + } + + setHeight(height: number) { + this.height = height + return this + } + + getHeight() { + return this.height + } + setCreatedAt(createdAt: Date) { this.createdAt = createdAt return this diff --git a/src/migrations/.snapshot-game.json b/src/migrations/.snapshot-game.json index 498b9b4..4c2a3c9 100644 --- a/src/migrations/.snapshot-game.json +++ b/src/migrations/.snapshot-game.json @@ -739,6 +739,28 @@ "length": 255, "mappedType": "string" }, + "width": { + "name": "width", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": null, + "default": "0", + "mappedType": "integer" + }, + "height": { + "name": "height", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": null, + "default": "0", + "mappedType": "integer" + }, "created_at": { "name": "created_at", "type": "datetime", diff --git a/src/migrations/Migration20250218160822.ts b/src/migrations/Migration20250219234315.ts similarity index 98% rename from src/migrations/Migration20250218160822.ts rename to src/migrations/Migration20250219234315.ts index daec3a8..6a787fe 100644 --- a/src/migrations/Migration20250218160822.ts +++ b/src/migrations/Migration20250219234315.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250218160822 extends Migration { +export class Migration20250219234315 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 Migration20250218160822 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, \`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 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 \`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\`);`); From c59b391a6a691baa19eb7516e50ebcddfab00ae5 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 21 Feb 2025 01:46:53 +0100 Subject: [PATCH 32/43] Stash --- public/assets.zip | Bin 141295 -> 148367 bytes src/commands/init.ts | 2 +- src/controllers/avatar.ts | 24 +++++- .../gameMaster/assetManager/sprite/update.ts | 74 +++++++++--------- 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/public/assets.zip b/public/assets.zip index a9981cf4b5b2553a0bba52287288c3a32f689a41..7ff57dbc8f13d766f9dfcb8023b521ededf36f05 100644 GIT binary patch delta 8118 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 delta 2205 zcmZWp3sh896n*DDm>Fhp1QbD0L_rB5nc*k$(HR(J20wsk5i-h{h?+mfOwtjGh^DTUnJfID#X?K*lb;+ATV;0No8dDxYu&kP-+lHz=bpFj+4`Z~ zNcb zp%`ET>8#5tHI~@GEV16JH2pI#vk~WJ3rahIi<84^0%z#vF3Mq*jljhWfFx_(`2pY~ z6Sz)yNVMjR!=c&C>3l7oJ^9Z2Lb$!46NmQdIC`l>8@8%`7so|F1dBUkz<>%3h~$%< zCMMUGHL7O=!^{ShX+b4!_9{v5ZiU>HVVFS@5&gfOm6C^TDsav2r_-osW|n-lqh8vi zu+KFyda3@~v`g{QU!)%_Tk~>ttTO4QF|sB0#s-h+pZ@jb-g7?o z%S_3oU3)I4hs<3a)!9;2cf2b)!{zYwC6e-Yv?U`7Tl{ZHGDbXbjqX3Gt+sk}?y1GB ze9GkwXJohMYr^Fq$eOeIy!&O^I@MB# zvXT8EAvxC9>&n6P?G4w`n&y3$*Le8dhpOP+icZ7k%8dmxw!@Pb;tjH->yI2)WNJF@ zR|aL+Jjv8H)yPMWyfwIZh_j?^O`_&Rg~^cFmU>8XGw(SQdtmf?pM31Pf9@XEQ!@G4 zxQyJ6E0;P&@%OJyz4Kj503MwVgB-vkpnhQ8Z+Ff_`(*mstTnyNu5v9==zp$R^!q8N>W>1$TJEOE zvQO-o6DIBFnX!0g>e}3@hSE{(T_MjjPEA!8R|Y)tE_B`L|1=rTN8zOH+ZPyhgKqql zP9=A%q+V0MZ0Mo28yGqWuZ0Ur#PItE)v@5VF>XcTBw902MJ@9Hr zkWm-5@o&~73Vb@W{6!ewnu|S8iTffBih+k5eOZlw(MfPxU=wD82RG&o@FD_Bp@Hot zl-!*;py+`f-1vF0kFc98ul9Hvq}lc$E`Kq+A?blg;V903CFJ*>ac%2hp+&I`v|LdR z#EZ=e9**4v3ITG4ZSbdEFL7WOnEJ>bwSrNKvnn8_J4DCbt$>z33j5=5wU1(K9TZy1 z;1fyDz`mZmI-MwD#I-Ihyhtsn8Z$nvOHwgCFeJe7+C7pM3`uvAxh- zUkV{Hea;A}75BmpDfvj!?MLkid0UKXH>wck0H34Tjrhy>>4ze3;s?U$8;B=;i4<#J zA?~=`o7m$*IgCQ75AhO4kkW^^kz@vMjv@Y-h zr?HA|E%-$nCVa4*{{O^9kehO&r*byfkZUIdh{|(uvM*Y5rQC8L>oW{GUkMlp@PI}v{*@Q zM_O{wQ6uZC!>IwZgcTD3)p4|h-2o(uy=q3G=o&~=Y_ZzH`6Yl1i}@o>G~nSGba07- z$Ve;-rZ=jHM*ng2TiyyLQN&4O1`m?J85f5T6-k-Qp9EoB2$@Febv(!=UenBBFAU&> z4@1aM;qvGjO8wTyp>jO+OAn=fw=JMyKJ|MXO8r*O;^`dnSkLc4c)Q-mme3yLfD*&QsRQ#iW`z?YS+@@V;P=(6NLb|gj&jY^IN~6Z9 zXtz!`11`TuQ3XY$wG|(#Xl&grG^qL5hcuQDC5oeH?021j8B&VY@o4WqfDKV3gB%c{ VGMdE81DF6t;leQG4urmle*trT?p^=@ diff --git a/src/commands/init.ts b/src/commands/init.ts index 62757b4..d34b853 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -489,7 +489,7 @@ export default class InitCommand extends BaseCommand { private async createCharacterHair(): Promise { const hairSprite = new Sprite() - hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1') + hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1').setWidth(30).setHeight(40) await hairSprite.save() const frontAction = new SpriteAction() diff --git a/src/controllers/avatar.ts b/src/controllers/avatar.ts index 803d607..a4e5984 100644 --- a/src/controllers/avatar.ts +++ b/src/controllers/avatar.ts @@ -65,9 +65,12 @@ export class AvatarController extends BaseController { return this.sendError(res, 'Body sprite file not found', 404) } + // Get body sprite metadata + const bodyMetadata = await sharp(bodySpritePath).metadata() + let avatar = sharp(bodySpritePath).extend({ - top: 2, - bottom: 2, + top: 0, + bottom: 0, background: { r: 0, g: 0, b: 0, alpha: 0 } }) @@ -76,7 +79,21 @@ export class AvatarController extends BaseController { if (characterHair?.sprite?.id) { const hairSpritePath = Storage.getPublicPath('sprites', characterHair.sprite.id, 'front.png') if (fs.existsSync(hairSpritePath)) { - avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }]) + // 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() + + avatar = avatar.composite([ + { + input: resizedHair, + left: 0, + top: -27 // Apply vertical offset + } + ]) } } } @@ -84,6 +101,7 @@ export class AvatarController extends BaseController { res.setHeader('Content-Type', 'image/png') return avatar.pipe(res) } catch (error) { + console.error('Avatar generation error:', error) return this.sendError(res, 'Error generating avatar', 500) } } diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 2e64f0f..91a3936 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -31,6 +31,8 @@ interface EffectiveDimensions { type Payload = { id: UUID name: string + width: number + height: number spriteActions: Array<{ action: string sprites: SpriteImage[] @@ -56,7 +58,7 @@ export default class SpriteUpdateEvent extends BaseEvent { await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) // Update sprite in database - await sprite.setName(data.name).setUpdatedAt(new Date()).save() + await sprite.setName(data.name).setWidth(data.width).setHeight(data.height).setUpdatedAt(new Date()).save() // First verify all sprite sheets can be generated for (const actionData of data.spriteActions) { @@ -89,13 +91,13 @@ export default class SpriteUpdateEvent extends BaseEvent { 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(await this.calculateMaxWidth(actionData.sprites)) + .setFrameHeight(totalHeight) + .setFrameRate(actionData.frameRate) await spriteRepository.getEntityManager().persistAndFlush(spriteAction) } @@ -126,27 +128,27 @@ export default class SpriteUpdateEvent extends BaseEvent { // 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, 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') - // 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 } - } + // 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() }) - .composite([{ input: buffer, left, top: verticalOffset }]) - .png() - .toBuffer() - }) ) // Combine frames into sprite sheet @@ -158,15 +160,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}` From 5b06386a39da0a52759a575733c18ae09831d6e0 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 21 Feb 2025 02:01:51 +0100 Subject: [PATCH 33/43] Finish sprite gen. update --- public/assets.zip | Bin 148367 -> 148603 bytes src/controllers/avatar.ts | 10 +- src/entities/base/sprite.ts | 12 +- .../gameMaster/assetManager/sprite/update.ts | 119 +++++++++++------- src/migrations/.snapshot-game.json | 6 +- ...19234315.ts => Migration20250221004940.ts} | 4 +- 6 files changed, 87 insertions(+), 64 deletions(-) rename src/migrations/{Migration20250219234315.ts => Migration20250221004940.ts} (98%) diff --git a/public/assets.zip b/public/assets.zip index 7ff57dbc8f13d766f9dfcb8023b521ededf36f05..bfbd77732fade717dc707fba371d02feaa0f632c 100644 GIT binary patch delta 1005 zcmZ9KT}YF06vlVX>t=H@M;591k(G*W&b>Of>2l2_LpP>rwq^x>Fj?kBv=M~m#X|Zb zh%h*T(Fb%Rh8R)1Fr$bdp)oI_ZYra?DUlQugb+LLw!-zo`19=dJkS5Uhxhs=O&KjNBC{j3Mk$^Ks%axc-`X<=&czxq`8mcukmIhvhZY~ng&rzcr_C|KfK~zel zRSic{NK6f9;u&W)bfu8lY$#VrnUzB5Od-J{co!$l+GJa0@}5~OSTm)J!t!5c!~&r# zGBklg!pH59N4d>VtP1hJ!awwH!X=+MWTofj3tHM4H+~kq8%T3Bs?UHw*fV_aot!oIt0zZ zxUx`A4cdihdy?teK}9#EzlIM1O?H5R(v7UXvnuv-_^gq`?#`rIFrrIX^2Svitr~GR zjn-lzmUK~-nMe9s%OhP&iUl8Rr;{cg>HRSY8|jM)J!tNh#0cK$OJvr2VJj_~cvsTm zD96HmYkm^zxG!MgzV7~nc<$%EUlz2(A{pxN0uF>EV5DdeG!iJK!g|a@$N7YRpdLH$ zZkT53dFc5tFEh+M{{1VuqEg;Fl%YPmQo2vqI}LLee?NsKLst}q~yfy!0q z&OqlN>=FbAlaS&FGi=iaht)BGFc3kR21qs+K%BtX^A}c^1o%EqSMKbVot@9FB4smE(=TqS_cU->(hxRhJDqP6i<$Q8*AV>+ zr3n^}A0Yn!d)nS>{7NfB`B>PN$xP{ZhGORBd|hU|{Ax!Pown1kcbod@nE9n(^ObuG z4wEas={3*eZ?)zNy{z4GTMRFr8uL?3N>BAa+2?fOzE(xy=VqbD zQj3Z#O$WO!cf41gyCc#+F+6rJ=7w=YS<2%fq;wS9Y=MKbHek=Z=}!F65loc{ws4y; zyJ2nzM*%c@Yr$}X9i(k!8+%CK$gTE}x{+NR-p=oI_*XYp3LW7Zd;9vY->_)EIzt#) z#uRrj*|naq{x&R@JIjQ=((3nFu?wCQfZwWvhiU=vAU_yWT)+t}Tp$3RY2XapIk?M}O?*MKXI#uI%^7Qt@J_W)CvKD?Tv z@_+~kNm_+q5k$ky6b6c6^HHq#qE#Y2X%+bzTu&p0IO0h!A(@QqioRK73nqEdlL8rY zQUWoGRbxdy*~Tb4@SHdON9{$}CU7cew{$pY-5ksYN2ctjTjm>x*891XB3kg7s6 Ovl 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\`);`); From 423dbd93f7d9b2802462533dd6900ca66b59f3a6 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 21 Feb 2025 22:05:46 +0100 Subject: [PATCH 34/43] Clear hair fix --- src/events/character/connect.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index 67256dd..fb7c1ed 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -9,7 +9,7 @@ import TeleportService from '@/services/characterTeleportService' interface CharacterConnectPayload { characterId: UUID - characterHairId?: UUID + characterHairId: UUID | null newNickname?: string } @@ -56,9 +56,11 @@ export default class CharacterConnectEvent extends BaseEvent { this.socket.characterId = character.id // Set character hair - if (data.characterHairId !== undefined) { + if (data.characterHairId !== undefined && data.characterHairId !== null) { const characterHair = await this.characterHairRepository.getById(data.characterHairId) await character.setCharacterHair(characterHair).save() + } else { + await character.setCharacterHair(null).save() } // Emit character connect event From 29ef089fcec314172a5e3674d837a2db30bc7c9d Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 21 Feb 2025 22:05:55 +0100 Subject: [PATCH 35/43] npm update --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b53c4bc..0cf0eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1949,9 +1949,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.3", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.3.tgz", - "integrity": "sha512-tWTeuO/BHDg6gKVnQJMjO42zkhsGss6s4bMdgJU24JVBT53yUvDjaO9H0L/BHKAtsMi4xlxkrDuMNSYWeHlekA==", + "version": "5.41.5", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.5.tgz", + "integrity": "sha512-WD0HQs6m4/aHJ9low7s737s2anZwFmnI7AhzMpBYua7EpKTTU1QDruHVHRmsqUKR/KdIcxC0fc/DkKI6+9INXg==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", From e8adb5c815e95ff7969015e23d5510b2ef677df7 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 21 Feb 2025 22:06:01 +0100 Subject: [PATCH 36/43] Clear hair fix --- src/application/zodTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/zodTypes.ts b/src/application/zodTypes.ts index a2010ab..c4da6a9 100644 --- a/src/application/zodTypes.ts +++ b/src/application/zodTypes.ts @@ -61,7 +61,7 @@ export const ZCharacterCreate = z.object({ export const ZCharacterConnect = z.object({ characterId: z.string(), - characterHairId: z.string().optional(), + characterHairId: z.string().optional().nullable(), newNickname: z .string() .min(3, { message: 'Name must be at least 3 characters long' }) From 12805e571ade32508c1c8b58842662bfd4d9c066 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 23 Feb 2025 01:39:30 +0100 Subject: [PATCH 37/43] revert --- .../gameMaster/assetManager/sprite/update.ts | 59 +++++-------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index b87d2aa..2e64f0f 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -31,8 +31,6 @@ interface EffectiveDimensions { type Payload = { id: UUID name: string - width: number | null - height: number | null spriteActions: Array<{ action: string sprites: SpriteImage[] @@ -57,17 +55,12 @@ export default class SpriteUpdateEvent extends BaseEvent { await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) - // 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() + // Update sprite in database + await sprite.setName(data.name).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, data.width ?? 0, data.height ?? 0))) { + if (!(await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action))) { return callback(false) } } @@ -85,12 +78,11 @@ 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 maximum dimensions - const maxWidth = data.width ?? Math.max(...effectiveDimensions.map((d) => d.width)) - const maxHeight = data.height ?? Math.max(...effectiveDimensions.map((d) => d.height)) + // Calculate total height needed for the sprite sheet + const maxHeight = 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 = data.height ?? maxHeight + maxTop + maxBottom + const totalHeight = maxHeight + maxTop + maxBottom const spriteAction = new SpriteAction() spriteAction.setSprite(sprite) @@ -101,7 +93,7 @@ export default class SpriteUpdateEvent extends BaseEvent { .setSprites(actionData.sprites) .setOriginX(actionData.originX) .setOriginY(actionData.originY) - .setFrameWidth(maxWidth) + .setFrameWidth(await this.calculateMaxWidth(actionData.sprites)) .setFrameHeight(totalHeight) .setFrameRate(actionData.frameRate) @@ -115,7 +107,7 @@ export default class SpriteUpdateEvent extends BaseEvent { } } - private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string, containerWidth: number, containerHeight: number): Promise { + private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise { try { if (!sprites.length) return true @@ -124,31 +116,25 @@ export default class SpriteUpdateEvent extends BaseEvent { const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate maximum dimensions - 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 maxWidth = Math.max(...effectiveDimensions.map((d) => d.width)) + const maxHeight = 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 = containerHeight > 0 ? containerHeight : maxHeight + maxTop + maxBottom + const totalHeight = maxHeight + maxTop + maxBottom // Process images and create sprite sheet const processedImages = await Promise.all( - sprites.map(async (sprite) => { + 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') - // 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) - + // Create individual frame + const left = offsetX >= 0 ? offsetX : 0 + const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0) return sharp({ create: { width: maxWidth, @@ -157,20 +143,7 @@ export default class SpriteUpdateEvent extends BaseEvent { 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' - } - : {}) - } - ]) + .composite([{ input: buffer, left, top: verticalOffset }]) .png() .toBuffer() }) From 2605295542cd31c97c703ad351ea8c40273672d5 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Thu, 6 Mar 2025 15:26:03 +0100 Subject: [PATCH 38/43] npm update --- package-lock.json | 136 +++++++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cf0eb1..45a1ae5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1073,14 +1073,14 @@ } }, "node_modules/@mikro-orm/cli": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.6.tgz", - "integrity": "sha512-sTMoDSJrnHZBT+ZAG40OeZwR9zRTYHtaaub9OoMM2CrxfI1KeiNqL/XFB4LaM5SVRAbnoEFpMJwQ8KS+5NcN9w==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.7.tgz", + "integrity": "sha512-jaEm8olUP7h/kz4AZyu21TJ8P1qAGgdyvFYG+J5eGAtPsdMN8ZMQJJy2m6DZcHNbuyllCVdzp3XT6MAzG+uwMw==", "license": "MIT", "dependencies": { "@jercle/yargonaut": "1.1.5", - "@mikro-orm/core": "6.4.6", - "@mikro-orm/knex": "6.4.6", + "@mikro-orm/core": "6.4.7", + "@mikro-orm/knex": "6.4.7", "fs-extra": "11.3.0", "tsconfig-paths": "4.2.0", "yargs": "17.7.2" @@ -1094,9 +1094,9 @@ } }, "node_modules/@mikro-orm/core": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.6.tgz", - "integrity": "sha512-xVm/ALG/3vTMgh6SrvojJ6jjMa0s2hNzWN0triDB16BaNdLwWE4aAaAe+3CuoMFqJAArSOUISTEjExbzELB1ZA==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.7.tgz", + "integrity": "sha512-ZePm7IRpW6/tGC6axCezI1/5YA3+MiDsbEj5KHgXDIxzHnftVfL3nbYlPlr0pW/UQSL5QcRqXxHIQz4P2OlFhg==", "license": "MIT", "dependencies": { "dataloader": "2.2.3", @@ -1104,7 +1104,7 @@ "esprima": "4.0.1", "fs-extra": "11.3.0", "globby": "11.1.0", - "mikro-orm": "6.4.6", + "mikro-orm": "6.4.7", "reflect-metadata": "0.2.2" }, "engines": { @@ -1115,9 +1115,9 @@ } }, "node_modules/@mikro-orm/knex": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.6.tgz", - "integrity": "sha512-o6t67tFH/GuPZCCEtKbTTL8HDXNgB2ITjButCTZLwteL0qI9yE/f7K6K+dEUKW+hAL3KRvc2BQeumvCVWFeISg==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.7.tgz", + "integrity": "sha512-IH2imlCzEzPyjMGmAn/9yEP4wRxKHczCiPfI7GWAVI2dRToox1MFpQzBW1x/m+3Dvwz8jXVakHVANN7KPZBi5w==", "license": "MIT", "dependencies": { "fs-extra": "11.3.0", @@ -1146,12 +1146,12 @@ } }, "node_modules/@mikro-orm/mariadb": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.6.tgz", - "integrity": "sha512-n6pOf69heOsbrggqYcf9SeF9hUdkw0FbzuUAcI72jWuyNRyzNR1UATblD+vRJnwt8JDWwakjINU/bduZbcEwPw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.7.tgz", + "integrity": "sha512-YICNykIbkzGHuY6Lk8s9pkafHgTxl1tY3C/UE2F6We1KAcOqr1XDkOBxHNS+Jadyw21LbWyY8XWHMxUSnfWg4w==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.6", + "@mikro-orm/knex": "6.4.7", "mariadb": "3.4.0" }, "engines": { @@ -1162,12 +1162,12 @@ } }, "node_modules/@mikro-orm/migrations": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.6.tgz", - "integrity": "sha512-i0/H07g1jQS0tKVSTSkHhrmuDEHxDD3/IzkiObezTgGlD5tqN7acaSr8RDJ3DgICb8MHUDVMLwxeGy8igDB4ag==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.7.tgz", + "integrity": "sha512-T5d5oPc+2pZq5tLn4PSFo5EGo/kp0l3x8YqLw3mJWetW8OKnUxmj6MPhboptycAHkc7SysakyRh3Oenn6brRXQ==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.6", + "@mikro-orm/knex": "6.4.7", "fs-extra": "11.3.0", "umzug": "3.8.2" }, @@ -1179,12 +1179,12 @@ } }, "node_modules/@mikro-orm/mysql": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.6.tgz", - "integrity": "sha512-KVP9Wif9MX/RrroVgYQQUrXe9SALBQLfB9CbuJlUB7MnEcZtDi5JNX7z5kghToz0aBrTtOgsr93G1bCoM0SJkg==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.7.tgz", + "integrity": "sha512-a5cc4Iy11PsbR2/cFV/5Ej+1ccd+xhBzlQjoCcDTbfJvncOt1F+FB/a4a11FInIVgDQl9wJSzbWOvarLM+SgmA==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.6", + "@mikro-orm/knex": "6.4.7", "mysql2": "3.12.0" }, "engines": { @@ -1195,9 +1195,9 @@ } }, "node_modules/@mikro-orm/reflection": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.6.tgz", - "integrity": "sha512-7mL7HFVnaOOhDNgLjjndWyeJUtOl2wKn0spSqB8uRjS4XtwNEGVZNkW5YD1t/x7TJ99wUhe+oRDiySciiJSeBQ==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.7.tgz", + "integrity": "sha512-toBQarVnyDsoZmMro4JUtUgmwuXGF2YDRl4W89J4FtxUyD1pCfCQKdSQEh9byB5W9cA9WQVyJkWqEYbZtpRQ6w==", "license": "MIT", "dependencies": { "globby": "11.1.0", @@ -1580,9 +1580,9 @@ } }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz", - "integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1605,9 +1605,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", - "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", + "version": "20.17.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.23.tgz", + "integrity": "sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -1949,9 +1949,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.5", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.5.tgz", - "integrity": "sha512-WD0HQs6m4/aHJ9low7s737s2anZwFmnI7AhzMpBYua7EpKTTU1QDruHVHRmsqUKR/KdIcxC0fc/DkKI6+9INXg==", + "version": "5.41.7", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.7.tgz", + "integrity": "sha512-eZbKJSx15bflfzKRiR+dKeLTr/M/YKb4cIp73OdU79PEMHQ6aEFUtbG6R+f0KvLLznI/O01G581U2Eqli6S2ew==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -1986,13 +1986,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -2617,9 +2617,9 @@ } }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -2757,17 +2757,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -3393,9 +3393,9 @@ } }, "node_modules/mariadb/node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -3478,9 +3478,9 @@ } }, "node_modules/mikro-orm": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.6.tgz", - "integrity": "sha512-Lr3uFK06O/4F/AtQAsuYD6QH7DgmUooSVFVGf1y02IuiKVFKOMJ4iKimkRMyoA+ykKhgYIp8WiaEqbWJVuz4Vw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.7.tgz", + "integrity": "sha512-lRB92yekDcdQEHloNY9LWfNLyKd3WajxPSe3jBvyVR9gMIO7YsjhqxL0mYvoQi8MQzXcUTi1EkWG05IM3gidIw==", "license": "MIT", "engines": { "node": ">= 18.12.0" @@ -3901,9 +3901,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -4145,9 +4145,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -4801,9 +4801,9 @@ } }, "node_modules/type-fest": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz", - "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.36.0.tgz", + "integrity": "sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -4826,9 +4826,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", From bfafd41c46e6006f87441e9d992a18e6893ebb79 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sat, 8 Mar 2025 00:48:22 +0100 Subject: [PATCH 39/43] npm update, mikro orm bug fix --- package-lock.json | 86 ++++++++++++++++++++--------------------- src/mikro-orm.config.ts | 8 ++-- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45a1ae5..c063cd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1073,14 +1073,14 @@ } }, "node_modules/@mikro-orm/cli": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.7.tgz", - "integrity": "sha512-jaEm8olUP7h/kz4AZyu21TJ8P1qAGgdyvFYG+J5eGAtPsdMN8ZMQJJy2m6DZcHNbuyllCVdzp3XT6MAzG+uwMw==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz", + "integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==", "license": "MIT", "dependencies": { "@jercle/yargonaut": "1.1.5", - "@mikro-orm/core": "6.4.7", - "@mikro-orm/knex": "6.4.7", + "@mikro-orm/core": "6.4.9", + "@mikro-orm/knex": "6.4.9", "fs-extra": "11.3.0", "tsconfig-paths": "4.2.0", "yargs": "17.7.2" @@ -1094,9 +1094,9 @@ } }, "node_modules/@mikro-orm/core": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.7.tgz", - "integrity": "sha512-ZePm7IRpW6/tGC6axCezI1/5YA3+MiDsbEj5KHgXDIxzHnftVfL3nbYlPlr0pW/UQSL5QcRqXxHIQz4P2OlFhg==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz", + "integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==", "license": "MIT", "dependencies": { "dataloader": "2.2.3", @@ -1104,7 +1104,7 @@ "esprima": "4.0.1", "fs-extra": "11.3.0", "globby": "11.1.0", - "mikro-orm": "6.4.7", + "mikro-orm": "6.4.9", "reflect-metadata": "0.2.2" }, "engines": { @@ -1115,9 +1115,9 @@ } }, "node_modules/@mikro-orm/knex": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.7.tgz", - "integrity": "sha512-IH2imlCzEzPyjMGmAn/9yEP4wRxKHczCiPfI7GWAVI2dRToox1MFpQzBW1x/m+3Dvwz8jXVakHVANN7KPZBi5w==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz", + "integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==", "license": "MIT", "dependencies": { "fs-extra": "11.3.0", @@ -1146,12 +1146,12 @@ } }, "node_modules/@mikro-orm/mariadb": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.7.tgz", - "integrity": "sha512-YICNykIbkzGHuY6Lk8s9pkafHgTxl1tY3C/UE2F6We1KAcOqr1XDkOBxHNS+Jadyw21LbWyY8XWHMxUSnfWg4w==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.9.tgz", + "integrity": "sha512-KuCzDGkC9cmNA8WxE9pZca6/Ds2sso3JUxiGoVyekOj/t9qer81UQYWasI80TBJ82TxrUdLM9NFBBO++tz+NYA==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.7", + "@mikro-orm/knex": "6.4.9", "mariadb": "3.4.0" }, "engines": { @@ -1162,12 +1162,12 @@ } }, "node_modules/@mikro-orm/migrations": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.7.tgz", - "integrity": "sha512-T5d5oPc+2pZq5tLn4PSFo5EGo/kp0l3x8YqLw3mJWetW8OKnUxmj6MPhboptycAHkc7SysakyRh3Oenn6brRXQ==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.9.tgz", + "integrity": "sha512-vwTXG8PU3bpzTZxxu1dWlhnUumM2Yob2IWajoh+Rj9+19VBZYc5N3tm8FRekt5oPzxeK5/9+sDfxT9FzXgjeNw==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.7", + "@mikro-orm/knex": "6.4.9", "fs-extra": "11.3.0", "umzug": "3.8.2" }, @@ -1179,13 +1179,13 @@ } }, "node_modules/@mikro-orm/mysql": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.7.tgz", - "integrity": "sha512-a5cc4Iy11PsbR2/cFV/5Ej+1ccd+xhBzlQjoCcDTbfJvncOt1F+FB/a4a11FInIVgDQl9wJSzbWOvarLM+SgmA==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.9.tgz", + "integrity": "sha512-rmHonMzvurB+50BNpKb9FORFVs3+V8S4Om1Tmv6MFvSdeD1Qqb/efvYp7cgv+NncHSrgMtKLMy3FDm7guU6yYQ==", "license": "MIT", "dependencies": { - "@mikro-orm/knex": "6.4.7", - "mysql2": "3.12.0" + "@mikro-orm/knex": "6.4.9", + "mysql2": "3.13.0" }, "engines": { "node": ">= 18.12.0" @@ -1195,9 +1195,9 @@ } }, "node_modules/@mikro-orm/reflection": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.7.tgz", - "integrity": "sha512-toBQarVnyDsoZmMro4JUtUgmwuXGF2YDRl4W89J4FtxUyD1pCfCQKdSQEh9byB5W9cA9WQVyJkWqEYbZtpRQ6w==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz", + "integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==", "license": "MIT", "dependencies": { "globby": "11.1.0", @@ -1674,9 +1674,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -2989,9 +2989,9 @@ } }, "node_modules/ioredis": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", - "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", "license": "MIT", "dependencies": { "@ioredis/commands": "^1.1.1", @@ -3478,9 +3478,9 @@ } }, "node_modules/mikro-orm": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.7.tgz", - "integrity": "sha512-lRB92yekDcdQEHloNY9LWfNLyKd3WajxPSe3jBvyVR9gMIO7YsjhqxL0mYvoQi8MQzXcUTi1EkWG05IM3gidIw==", + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz", + "integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==", "license": "MIT", "engines": { "node": ">= 18.12.0" @@ -3579,9 +3579,9 @@ } }, "node_modules/mysql2": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", - "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz", + "integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -4801,9 +4801,9 @@ } }, "node_modules/type-fest": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.36.0.tgz", - "integrity": "sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz", + "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" diff --git a/src/mikro-orm.config.ts b/src/mikro-orm.config.ts index 1274891..f1844ef 100644 --- a/src/mikro-orm.config.ts +++ b/src/mikro-orm.config.ts @@ -1,15 +1,15 @@ import serverConfig from '@/application/config' -import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb' -// import { defineConfig, MySqlDriver } from '@mikro-orm/mysql' import { Migrator } from '@mikro-orm/migrations' +// import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb' +import { defineConfig, MySqlDriver } from '@mikro-orm/mysql' import { TsMorphMetadataProvider } from '@mikro-orm/reflection' export default defineConfig({ extensions: [Migrator], metadataProvider: TsMorphMetadataProvider, - entities: ['./dist/entities/*.js'], + entities: ['./src/entities/*.ts'], entitiesTs: ['./src/entities/*.ts'], - driver: MariaDbDriver, + driver: MySqlDriver, host: serverConfig.DB_HOST, port: serverConfig.DB_PORT, user: serverConfig.DB_USER, From c9e8b29f112568ee651960856a7696cf2f5f788c Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sat, 8 Mar 2025 01:40:23 +0100 Subject: [PATCH 40/43] Simplified code --- .../gameMaster/assetManager/sprite/update.ts | 208 +++++++++++------- 1 file changed, 124 insertions(+), 84 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 2e64f0f..530d6dc 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -28,82 +28,109 @@ interface EffectiveDimensions { bottom: number } -type Payload = { +interface SpriteActionData { + action: string + sprites: SpriteImage[] + originX: number + originY: number + frameRate: number +} + +interface UpdateSpritePayload { id: UUID name: string - spriteActions: Array<{ - action: string - sprites: SpriteImage[] - originX: number - originY: number - frameRate: number - }> + 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: Payload, callback: (success: boolean) => void): Promise { + private async handleEvent(data: UpdateSpritePayload, callback: (success: boolean) => void): Promise { try { - if (!(await this.isCharacterGM())) return + if (!(await this.isCharacterGM())) { + callback(false) + return + } - const spriteRepository = new SpriteRepository() - const sprite = await spriteRepository.getById(data.id) - if (!sprite) return callback(false) + const sprite = await this.spriteRepository.getById(data.id) + if (!sprite) { + callback(false) + return + } - await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) + await this.spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) // Update sprite in database await sprite.setName(data.name).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))) { - return callback(false) - } + const allSheetsGenerated = await this.verifyAllSpriteSheets(data.spriteActions, sprite.getId()) + if (!allSheetsGenerated) { + callback(false) + return } - const existingActions = sprite.getSpriteActions() - - // Remove existing actions only after confirming sprite sheets generated successfully - for (const existingAction of existingActions) { - await spriteRepository.getEntityManager().removeAndFlush(existingAction) - } + // Remove existing actions + await this.removeExistingActions(sprite) // Create new actions - for (const actionData of data.spriteActions) { - // 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)) + await this.createNewSpriteActions(data.spriteActions, sprite) - // Calculate total height needed for the sprite sheet - const maxHeight = 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 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) - - await spriteRepository.getEntityManager().persistAndFlush(spriteAction) - } - - return callback(true) + callback(true) } catch (error) { console.error(`Error updating sprite ${data.id}:`, error) - return callback(false) + callback(false) + } + } + + private async verifyAllSpriteSheets(actionsData: SpriteActionData[], spriteId: string): Promise { + for (const actionData of actionsData) { + if (!(await this.generateSpriteSheet(actionData.sprites, spriteId, actionData.action))) { + return false + } + } + return true + } + + private async removeExistingActions(sprite: any): Promise { + const existingActions = sprite.getSpriteActions() + for (const existingAction of existingActions) { + await this.spriteRepository.getEntityManager().removeAndFlush(existingAction) + } + } + + private async createNewSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise { + 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) } } @@ -116,20 +143,19 @@ 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 maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) - const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) + 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) // Calculate total height needed const totalHeight = maxHeight + maxTop + maxBottom // Process images and create sprite sheet const processedImages = await Promise.all( - sprites.map(async (sprite, index) => { + 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 uri = this.extractBase64Data(sprite.url) const buffer = Buffer.from(uri, 'base64') // Create individual frame @@ -150,30 +176,10 @@ export default class SpriteUpdateEvent extends BaseEvent { ) // Combine frames into sprite sheet - const spriteSheet = await sharp({ - create: { - width: maxWidth * sprites.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 - })) - ) - .png() - .toBuffer() - - // Ensure directory exists - const dir = `public/sprites/${spriteId}` - await fs.promises.mkdir(dir, { recursive: true }) + const spriteSheet = await this.createSpriteSheetImage(processedImages, maxWidth, totalHeight) // Save the sprite sheet - await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) + await this.saveSpriteSheet(spriteId, action, spriteSheet) return true } catch (error) { @@ -182,9 +188,43 @@ export default class SpriteUpdateEvent extends BaseEvent { } } - private async processImage(sprite: SpriteImage): Promise { - const uri = sprite.url.split(';base64,').pop() + private async createSpriteSheetImage(processedImages: Buffer[], maxWidth: number, totalHeight: number): Promise { + 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 + })) + ) + .png() + .toBuffer() + } + + private async saveSpriteSheet(spriteId: string, action: string, spriteSheet: Buffer): Promise { + // Ensure directory exists + const dir = `public/sprites/${spriteId}` + await fs.promises.mkdir(dir, { recursive: true }) + + // Save the sprite sheet + await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) + } + + private extractBase64Data(url: string): string { + const uri = url.split(';base64,').pop() if (!uri) throw new Error('Invalid base64 image') + return uri + } + + private async processImage(sprite: SpriteImage): Promise { + const uri = this.extractBase64Data(sprite.url) const buffer = Buffer.from(uri, 'base64') const metadata = await sharp(buffer).metadata() return { @@ -212,6 +252,6 @@ export default class SpriteUpdateEvent extends BaseEvent { const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate maximum width needed - return Math.max(...effectiveDimensions.map((d) => d.width)) + return Math.max(...effectiveDimensions.map((d) => d.width), 0) } } From 5ca41cfd38e9d44cb15e49dd5b8003bb9785f4c5 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 11 Mar 2025 23:41:42 +0100 Subject: [PATCH 41/43] sprite work --- package-lock.json | 288 +++++++++--------- .../gameMaster/assetManager/sprite/update.ts | 285 +++++++---------- 2 files changed, 251 insertions(+), 322 deletions(-) diff --git a/package-lock.json b/package-lock.json index c063cd5..403dcff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,14 +59,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -96,13 +96,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -127,17 +127,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -146,9 +146,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], @@ -466,9 +466,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], @@ -483,9 +483,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", "cpu": [ "arm64" ], @@ -500,9 +500,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], @@ -517,9 +517,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], @@ -551,9 +551,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], @@ -585,9 +585,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], @@ -602,9 +602,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], @@ -1324,9 +1324,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", - "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.12.0.tgz", + "integrity": "sha512-QSwwzgzWoil1SCQse+yCHwlhRxNv2dX9siPnAb9zR/UmMhac4mjMrlMZpk64BlCeOFi1kJKgXRkihSwRMbboAQ==", "license": "MIT", "dependencies": { "ajv": "~8.13.0", @@ -1375,12 +1375,12 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.0.tgz", - "integrity": "sha512-vXQPRQ+vJJn4GVqxkwRe+UGgzNxdV8xuJZY2zem46Y0p3tlahucH9/hPmLGj2i9dQnUBFiRnoM9/KW7PYw8F4Q==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.1.tgz", + "integrity": "sha512-3vgJYwumcjoDOXU3IxZfd616lqOdmr8Ezj4OWgJZfhmiBK4Nh7eWcv8sU8N/HdzXcuHDXCRGn/6O2Q75QvaZMA==", "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.12.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1408,12 +1408,12 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.5.tgz", - "integrity": "sha512-jg70HfoK44KfSP3MTiL5rxsZH7X1ktX3cZs9Sl8eDu1/LxJSbPsh0MOFRC710lIuYYSgxWjI5AjbCBAl7u3RxA==", + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.6.tgz", + "integrity": "sha512-7WepygaF3YPEoToh4MAL/mmHkiIImQq3/uAkQX46kVoKTNOOlCtFGyNnze6OYuWw2o9rxsyrHVfIBKxq/am2RA==", "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.15.0", + "@rushstack/terminal": "0.15.1", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1605,9 +1605,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.23.tgz", - "integrity": "sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==", + "version": "20.17.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -1949,9 +1949,9 @@ } }, "node_modules/bullmq": { - "version": "5.41.7", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.7.tgz", - "integrity": "sha512-eZbKJSx15bflfzKRiR+dKeLTr/M/YKb4cIp73OdU79PEMHQ6aEFUtbG6R+f0KvLLznI/O01G581U2Eqli6S2ew==", + "version": "5.41.9", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.9.tgz", + "integrity": "sha512-dcmBEo6CzLh3PQ7KN6qz5M8VsSv49vcXfEadgrUdfmUvYDpg630/yPV/Ov18unPT4IOCjz1XZSgpjs5DjwlBYw==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -2438,9 +2438,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2451,31 +2451,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escalade": { @@ -3346,9 +3346,9 @@ "license": "ISC" }, "node_modules/lru.min": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", - "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -3393,9 +3393,9 @@ } }, "node_modules/mariadb/node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 530d6dc..995bee4 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -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 { 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 { - for (const actionData of actionsData) { - if (!(await this.generateSpriteSheet(actionData.sprites, spriteId, actionData.action))) { - return false - } - } - return true - } - - private async removeExistingActions(sprite: any): Promise { - const existingActions = sprite.getSpriteActions() - for (const existingAction of existingActions) { - await this.spriteRepository.getEntityManager().removeAndFlush(existingAction) - } - } - - private async createNewSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise { - 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 { + private async processAllSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise { 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 { - 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 { - // 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 { - 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 { - 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) - } -} +} \ No newline at end of file From 36c9522e8a9b94da6fac591cf0b119ec259ae305 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 11 Mar 2025 23:53:27 +0100 Subject: [PATCH 42/43] Improvements --- .../gameMaster/assetManager/sprite/update.ts | 136 ++++++++++-------- src/mikro-orm.config.ts | 6 +- 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 995bee4..7b3b319 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -76,12 +76,8 @@ export default class SpriteUpdateEvent extends BaseEvent { 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 + const frameDimensions = await this.generateAndSaveSpriteSheet(actionData.sprites, sprite.getId(), actionData.action) + if (!frameDimensions) return false // Create and save sprite action const spriteAction = new SpriteAction() @@ -89,13 +85,13 @@ export default class SpriteUpdateEvent extends BaseEvent { 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) + .setAction(actionData.action) + .setSprites(actionData.sprites) + .setOriginX(actionData.originX) + .setOriginY(actionData.originY) + .setFrameWidth(frameDimensions.frameWidth) + .setFrameHeight(frameDimensions.frameHeight) + .setFrameRate(actionData.frameRate) await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction) } @@ -107,80 +103,96 @@ export default class SpriteUpdateEvent extends BaseEvent { } } - private async generateAndSaveSpriteSheet( - sprites: SpriteImage[], - spriteId: string, - action: string - ): Promise<{ width: number, height: number } | null> { + private async generateAndSaveSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<{ frameWidth: number; frameHeight: number } | null> { try { - if (sprites.length === 0) return { width: 0, height: 0 } + if (sprites.length === 0) return { frameWidth: 0, frameHeight: 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') - }) + // Extract image data and get image metadata + const imagesData = await Promise.all( + sprites.map(async (sprite) => { + const base64Data = sprite.url.split(';base64,').pop() + if (!base64Data) throw new Error('Invalid base64 image') + const buffer = Buffer.from(base64Data, 'base64') + const metadata = await sharp(buffer).metadata() + + return { + buffer, + width: metadata.width || 0, + height: metadata.height || 0 + } + }) ) - // 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') + // Skip creation if any image has invalid dimensions + if (imagesData.some((data) => data.width === 0 || data.height === 0)) { + console.error('One or more sprites have invalid 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() + // Calculate the maximum width and height to use for all frames + const maxWidth = Math.max(...imagesData.map((data) => data.width)) + const maxHeight = Math.max(...imagesData.map((data) => data.height)) + + // Create frames of uniform size with the original sprites centered + const uniformFrames = await Promise.all( + imagesData.map(async (imageData) => { + // Calculate centering offsets to position the sprite in the middle of the frame + const xOffset = Math.floor((maxWidth - imageData.width) / 2) + const yOffset = Math.floor((maxHeight - imageData.height) / 2) + + // Create a uniform-sized frame with the sprite centered + return sharp({ + create: { + width: maxWidth, + height: maxHeight, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 } + } }) + .composite([ + { + input: imageData.buffer, + left: xOffset, + top: yOffset + } + ]) + .png() + .toBuffer() + }) ) - // Create sprite sheet with uniformly sized frames + // Create the sprite sheet with uniform frames const spriteSheet = await sharp({ create: { - width: maxWidth * resizedFrames.length, + width: maxWidth * uniformFrames.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() + .composite( + uniformFrames.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 } + // Return the uniform frame dimensions + return { + frameWidth: maxWidth, + frameHeight: maxHeight + } } catch (error) { console.error('Error generating sprite sheet:', error) return null } } -} \ No newline at end of file +} diff --git a/src/mikro-orm.config.ts b/src/mikro-orm.config.ts index f1844ef..5f21b31 100644 --- a/src/mikro-orm.config.ts +++ b/src/mikro-orm.config.ts @@ -1,7 +1,7 @@ import serverConfig from '@/application/config' import { Migrator } from '@mikro-orm/migrations' -// import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb' -import { defineConfig, MySqlDriver } from '@mikro-orm/mysql' +import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb' +// import { defineConfig, MySqlDriver } from '@mikro-orm/mysql' import { TsMorphMetadataProvider } from '@mikro-orm/reflection' export default defineConfig({ @@ -9,7 +9,7 @@ export default defineConfig({ metadataProvider: TsMorphMetadataProvider, entities: ['./src/entities/*.ts'], entitiesTs: ['./src/entities/*.ts'], - driver: MySqlDriver, + driver: MariaDbDriver, host: serverConfig.DB_HOST, port: serverConfig.DB_PORT, user: serverConfig.DB_USER, From 9f42d1e59ddfe6383826729f7f8011185bccdb4f Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 12 Mar 2025 13:46:51 +0100 Subject: [PATCH 43/43] find the global maximum dimensions across all actions --- .../gameMaster/assetManager/sprite/update.ts | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 7b3b319..4829b84 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -71,12 +71,43 @@ export default class SpriteUpdateEvent extends BaseEvent { await this.spriteRepository.getEntityManager().removeAndFlush(existingAction) } - // Process each action + // First pass: find the global maximum dimensions across all actions + let globalMaxWidth = 0 + let globalMaxHeight = 0 + + // Extract all image metadata to find global maximums for (const actionData of actionsData) { if (actionData.sprites.length === 0) continue - // Generate and save the sprite sheet - const frameDimensions = await this.generateAndSaveSpriteSheet(actionData.sprites, sprite.getId(), actionData.action) + const imagesData = await Promise.all( + actionData.sprites.map(async (sprite) => { + const base64Data = sprite.url.split(';base64,').pop() + if (!base64Data) throw new Error('Invalid base64 image') + const buffer = Buffer.from(base64Data, 'base64') + const metadata = await sharp(buffer).metadata() + + return { + width: metadata.width || 0, + height: metadata.height || 0 + } + }) + ) + + // Update global maximums with this action's maximums + const actionMaxWidth = Math.max(...imagesData.map((data) => data.width), 0) + const actionMaxHeight = Math.max(...imagesData.map((data) => data.height), 0) + + globalMaxWidth = Math.max(globalMaxWidth, actionMaxWidth) + globalMaxHeight = Math.max(globalMaxHeight, actionMaxHeight) + } + + // Process each action using the global maximum dimensions + for (const actionData of actionsData) { + if (actionData.sprites.length === 0) continue + + // Generate and save the sprite sheet using global dimensions + const frameDimensions = await this.generateAndSaveSpriteSheet(actionData.sprites, sprite.getId(), actionData.action, globalMaxWidth, globalMaxHeight) + if (!frameDimensions) return false // Create and save sprite action @@ -103,11 +134,11 @@ export default class SpriteUpdateEvent extends BaseEvent { } } - private async generateAndSaveSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<{ frameWidth: number; frameHeight: number } | null> { + private async generateAndSaveSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string, maxWidth: number, maxHeight: number): Promise<{ frameWidth: number; frameHeight: number } | null> { try { if (sprites.length === 0) return { frameWidth: 0, frameHeight: 0 } - // Extract image data and get image metadata + // Extract image data const imagesData = await Promise.all( sprites.map(async (sprite) => { const base64Data = sprite.url.split(';base64,').pop() @@ -129,10 +160,6 @@ export default class SpriteUpdateEvent extends BaseEvent { return null } - // Calculate the maximum width and height to use for all frames - const maxWidth = Math.max(...imagesData.map((data) => data.width)) - const maxHeight = Math.max(...imagesData.map((data) => data.height)) - // Create frames of uniform size with the original sprites centered const uniformFrames = await Promise.all( imagesData.map(async (imageData) => { @@ -185,7 +212,7 @@ export default class SpriteUpdateEvent extends BaseEvent { await fs.promises.mkdir(dir, { recursive: true }) await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) - // Return the uniform frame dimensions + // Return the uniform frame dimensions (now global maximum dimensions) return { frameWidth: maxWidth, frameHeight: maxHeight