diff --git a/src/application/base/baseCommand.ts b/src/application/base/baseCommand.ts new file mode 100644 index 0000000..2ef9dfa --- /dev/null +++ b/src/application/base/baseCommand.ts @@ -0,0 +1,5 @@ +import { Server } from 'socket.io' + +export abstract class BaseCommand { + constructor(readonly io: Server) {} +} diff --git a/src/application/base/baseEntity.ts b/src/application/base/baseEntity.ts index b97ca81..c03bb74 100644 --- a/src/application/base/baseEntity.ts +++ b/src/application/base/baseEntity.ts @@ -1,4 +1,5 @@ import { EntityManager } from '@mikro-orm/core' + import Database from '#application/database' import { appLogger } from '#application/logger' @@ -8,18 +9,18 @@ export abstract class BaseEntity { } async save(): Promise { - return this.performDbOperation('persist', 'save entity') + return this.execute('persist', 'save entity') } async update(): Promise { - return this.performDbOperation('merge', 'update entity') + return this.execute('merge', 'update entity') } async delete(): Promise { - return this.performDbOperation('remove', 'remove entity') + return this.execute('remove', 'remove entity') } - private async performDbOperation(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise { + private async execute(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise { try { const em = this.getEntityManager() @@ -38,4 +39,4 @@ export abstract class BaseEntity { throw error } } -} \ No newline at end of file +} diff --git a/src/application/base/baseEvent.ts b/src/application/base/baseEvent.ts index 74534b6..7ab8f0a 100644 --- a/src/application/base/baseEvent.ts +++ b/src/application/base/baseEvent.ts @@ -1,4 +1,10 @@ +import { Server } from 'socket.io' + +import { TSocket } from '#application/types' export abstract class BaseEvent { - -} \ No newline at end of file + constructor( + readonly io: Server, + readonly socket: TSocket + ) {} +} diff --git a/src/application/base/baseRepository.ts b/src/application/base/baseRepository.ts index ad2c539..cb22eae 100644 --- a/src/application/base/baseRepository.ts +++ b/src/application/base/baseRepository.ts @@ -1,12 +1,9 @@ -import { EntityManager, MikroORM } from '@mikro-orm/core' +import { EntityManager } from '@mikro-orm/core' + import Database from '../database' export abstract class BaseRepository { - protected get orm(): MikroORM { - return Database.getORM() - } - protected get em(): EntityManager { return Database.getEntityManager() } -} \ No newline at end of file +} diff --git a/src/application/character/aStar.ts b/src/application/character/aStar.ts deleted file mode 100644 index 104ea81..0000000 --- a/src/application/character/aStar.ts +++ /dev/null @@ -1,70 +0,0 @@ -import config from '../config' - -type Position = { x: number; y: number } -export type Node = Position & { parent?: Node; g: number; h: number; f: number } - -export class AStar { - private static readonly DIRECTIONS = [ - { x: 0, y: -1 }, // Up - { x: 0, y: 1 }, // Down - { x: -1, y: 0 }, // Left - { x: 1, y: 0 }, // Right - { x: -1, y: -1 }, - { x: -1, y: 1 }, - { x: 1, y: -1 }, - { x: 1, y: 1 } - ] - - static findPath(start: Position, end: Position, grid: number[][]): Node[] { - const openList: Node[] = [{ ...start, g: 0, h: 0, f: 0 }] - const closedSet = new Set() - const getKey = (p: Position) => `${p.x},${p.y}` - - while (openList.length > 0) { - const current = openList.reduce((min, node) => (node.f < min.f ? node : min)) - if (current.x === end.x && current.y === end.y) return this.reconstructPath(current) - - openList.splice(openList.indexOf(current), 1) - closedSet.add(getKey(current)) - - const neighbors = this.DIRECTIONS.slice(0, config.ALLOW_DIAGONAL_MOVEMENT ? 8 : 4) - .map((dir) => ({ x: current.x + dir.x, y: current.y + dir.y })) - .filter((pos) => this.isValidPosition(pos, grid, end)) - - for (const neighbor of neighbors) { - if (closedSet.has(getKey(neighbor))) continue - - const g = current.g + this.getDistance(current, neighbor) - const existing = openList.find((node) => node.x === neighbor.x && node.y === neighbor.y) - - if (!existing || g < existing.g) { - const h = this.getDistance(neighbor, end) - const node: Node = { ...neighbor, g, h, f: g + h, parent: current } - if (!existing) openList.push(node) - else Object.assign(existing, node) - } - } - } - - return [] // No path found - } - - private static isValidPosition(pos: Position, grid: number[][], end: Position): boolean { - return pos.x >= 0 && pos.y >= 0 && pos.x < grid[0].length && pos.y < grid.length && (grid[pos.y][pos.x] === 0 || (pos.x === end.x && pos.y === end.y)) - } - - private static getDistance(a: Position, b: Position): number { - const dx = Math.abs(a.x - b.x), - dy = Math.abs(a.y - b.y) - // Manhattan distance for straight paths, then Euclidean for diagonals - return dx + dy + (Math.sqrt(2) - 2) * Math.min(dx, dy) - } - - private static reconstructPath(endNode: Node): Node[] { - const path: Node[] = [] - for (let current: Node | undefined = endNode; current; current = current.parent) { - path.unshift(current) - } - return path - } -} diff --git a/src/application/character/rotation.ts b/src/application/character/rotation.ts deleted file mode 100644 index ddf3a43..0000000 --- a/src/application/character/rotation.ts +++ /dev/null @@ -1,33 +0,0 @@ -import config from '../config' - -class Rotation { - static calculate(X1: number, Y1: number, X2: number, Y2: number): number { - if (config.ALLOW_DIAGONAL_MOVEMENT) { - // Check diagonal movements - if (X1 > X2 && Y1 > Y2) { - return 7 - } else if (X1 < X2 && Y1 < Y2) { - return 3 - } else if (X1 > X2 && Y1 < Y2) { - return 5 - } else if (X1 < X2 && Y1 > Y2) { - return 1 - } - } - - // Non-diagonal movements - if (X1 > X2) { - return 6 - } else if (X1 < X2) { - return 2 - } else if (Y1 < Y2) { - return 4 - } else if (Y1 > Y2) { - return 0 - } - - return 0 // Default case - } -} - -export default Rotation diff --git a/src/application/chat.ts b/src/application/chat.ts deleted file mode 100644 index 416b89f..0000000 --- a/src/application/chat.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function isCommand(message: string, command?: string) { - if (command) { - return message === `/${command}` || message.startsWith(`/${command} `) - } - return message.startsWith('/') -} - -export function getArgs(command: string, message: string): string[] | undefined { - if (!isCommand(message, command)) return - return message.split(`/${command} `)[1].split(' ') -} diff --git a/src/application/database.ts b/src/application/database.ts index 118a3e8..bd54213 100644 --- a/src/application/database.ts +++ b/src/application/database.ts @@ -1,5 +1,6 @@ -import { MikroORM } from '@mikro-orm/mysql' import { EntityManager } from '@mikro-orm/core' +import { MikroORM } from '@mikro-orm/mysql' + import { appLogger } from './logger' import config from '../../mikro-orm.config' @@ -33,4 +34,4 @@ class Database { } } -export default Database \ No newline at end of file +export default Database diff --git a/src/application/types.ts b/src/application/types.ts index 1ebc35f..2ebd239 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -41,7 +41,7 @@ export type AssetData = { frameRate?: number frameWidth?: number frameHeight?: number - frameRate?: number + frameCount?: number } export type WorldSettings = { diff --git a/src/application/zone.ts b/src/application/zone.ts deleted file mode 100644 index d8d39ec..0000000 --- a/src/application/zone.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function FlattenZoneArray(tiles: string[][]) { - const normalArray = [] - - for (const row of tiles) { - normalArray.push(...row) - } - - return normalArray -} diff --git a/src/commands/alert.ts b/src/commands/alert.ts index ab9cce3..2998f79 100644 --- a/src/commands/alert.ts +++ b/src/commands/alert.ts @@ -1,10 +1,10 @@ import { Server } from 'socket.io' +import { BaseCommand } from '#application/base/baseCommand' + type CommandInput = string[] -export default class AlertCommand { - constructor(private readonly io: Server) {} - +export default class AlertCommand extends BaseCommand { public execute(input: CommandInput): void { const message: string = input.join(' ') ?? null if (!message) return console.log('message is required') diff --git a/src/commands/init.ts b/src/commands/init.ts index 42ac846..325ea5b 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -3,6 +3,7 @@ import fs from 'fs' import sharp from 'sharp' import { Server } from 'socket.io' +import { BaseCommand } from '#application/base/baseCommand' import { CharacterGender, CharacterRace } from '#application/enums' import { getPublicPath } from '#application/storage' import { UUID } from '#application/types' @@ -23,9 +24,7 @@ import ZoneRepository from '#repositories/zoneRepository' // @TODO : Replace this with seeding // https://mikro-orm.io/docs/seeding -export default class InitCommand { - constructor(private readonly io: Server) {} - +export default class InitCommand extends BaseCommand { public async execute(): Promise { // Assets await this.importTiles() diff --git a/src/commands/listZones.ts b/src/commands/listZones.ts index a535c9d..8a56ab7 100644 --- a/src/commands/listZones.ts +++ b/src/commands/listZones.ts @@ -1,12 +1,11 @@ import { Server } from 'socket.io' +import { BaseCommand } from '#application/base/baseCommand' import ZoneManager from '#managers/zoneManager' type CommandInput = string[] -export default class ListZonesCommand { - constructor(private readonly io: Server) {} - +export default class ListZonesCommand extends BaseCommand { public execute(input: CommandInput): void { console.log(ZoneManager.getLoadedZones()) } diff --git a/src/commands/tiles.ts b/src/commands/tiles.ts index ab2af40..335c82a 100644 --- a/src/commands/tiles.ts +++ b/src/commands/tiles.ts @@ -4,12 +4,11 @@ import path from 'path' import sharp from 'sharp' import { Server } from 'socket.io' +import { BaseCommand } from '#application/base/baseCommand' import { commandLogger } from '#application/logger' import { getPublicPath } from '#application/storage' -export default class TilesCommand { - constructor(private readonly io: Server) {} - +export default class TilesCommand extends BaseCommand { public async execute(): Promise { // Get all tiles const tilesDir = getPublicPath('tiles') diff --git a/src/entities/user.ts b/src/entities/user.ts index c2f3501..dac39fe 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -6,7 +6,6 @@ import { PasswordResetToken } from './passwordResetToken' import { BaseEntity } from '#application/base/baseEntity' - @Entity() export class User extends BaseEntity { @PrimaryKey() diff --git a/src/managers/queueManager.ts b/src/managers/queueManager.ts index 09e3562..6d79948 100644 --- a/src/managers/queueManager.ts +++ b/src/managers/queueManager.ts @@ -9,7 +9,6 @@ import { queueLogger } from '#application/logger' import { getAppPath } from '#application/storage' import { TSocket } from '#application/types' - class QueueManager { private connection!: IORedis private queue!: Queue diff --git a/src/models/loadedZone.ts b/src/models/loadedZone.ts index b579b85..2b216b8 100644 --- a/src/models/loadedZone.ts +++ b/src/models/loadedZone.ts @@ -34,6 +34,7 @@ class LoadedZone { } public getCharactersInZone(): ZoneCharacter[] { + console.log(this.characters) return this.characters } diff --git a/src/repositories/characterHairRepository.ts b/src/repositories/characterHairRepository.ts index a5dd30e..fd48ac6 100644 --- a/src/repositories/characterHairRepository.ts +++ b/src/repositories/characterHairRepository.ts @@ -6,37 +6,37 @@ class CharacterHairRepository extends BaseRepository { async getFirst() { try { const repository = this.em.getRepository(CharacterHair) - return await repository.findOne({ id: { $exists: true } }, { populate: ['*'] }) + return await repository.findOne({ id: { $exists: true } }) } catch (error: any) { appLogger.error(`Failed to get first character hair: ${error instanceof Error ? error.message : String(error)}`) return null } } - async getAll() { + async getAll(): Promise { try { const repository = this.em.getRepository(CharacterHair) - return await repository.findAll({ populate: ['*'] }) + return await repository.findAll() } catch (error: any) { appLogger.error(`Failed to get all character hair: ${error instanceof Error ? error.message : String(error)}`) - return null + return [] } } - async getAllSelectable() { + async getAllSelectable(): Promise { try { const repository = this.em.getRepository(CharacterHair) - return await repository.find({ isSelectable: true }, { populate: ['*'] }) + return await repository.find({ isSelectable: true }) } catch (error: any) { appLogger.error(`Failed to get selectable character hair: ${error instanceof Error ? error.message : String(error)}`) - return null + return [] } } - async getById(id: number) { + async getById(id: number): Promise { try { const repository = this.em.getRepository(CharacterHair) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { appLogger.error(`Failed to get character hair by ID: ${error instanceof Error ? error.message : String(error)}`) return null diff --git a/src/repositories/characterRepository.ts b/src/repositories/characterRepository.ts index 10ffc19..229eb19 100644 --- a/src/repositories/characterRepository.ts +++ b/src/repositories/characterRepository.ts @@ -6,7 +6,7 @@ class CharacterRepository extends BaseRepository { async getByUserId(userId: number): Promise { try { const repository = this.em.getRepository(Character) - return await repository.find({ user: userId }, { populate: ['*'] }) + return await repository.find({ user: userId }) } catch (error: any) { appLogger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) return [] @@ -16,17 +16,17 @@ class CharacterRepository extends BaseRepository { async getByUserAndId(userId: number, characterId: number): Promise { try { const repository = this.em.getRepository(Character) - return await repository.findOne({ user: userId, id: characterId }, { populate: ['*'] }) + return await repository.findOne({ user: userId, id: characterId }) } catch (error: any) { appLogger.error(`Failed to get character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`) return null } } - async getById(id: number): Promise { + async getById(id: number, populate?: string[]): Promise { try { const repository = this.em.getRepository(Character) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { appLogger.error(`Failed to get character by ID: ${error instanceof Error ? error.message : String(error)}`) return null @@ -36,7 +36,7 @@ class CharacterRepository extends BaseRepository { async getByName(name: string): Promise { try { const repository = this.em.getRepository(Character) - return await repository.findOne({ name }, { populate: ['*'] }) + return await repository.findOne({ name }) } catch (error: any) { appLogger.error(`Failed to get character by name: ${error instanceof Error ? error.message : String(error)}`) return null diff --git a/src/repositories/characterTypeRepository.ts b/src/repositories/characterTypeRepository.ts index bf13eb1..635e892 100644 --- a/src/repositories/characterTypeRepository.ts +++ b/src/repositories/characterTypeRepository.ts @@ -6,7 +6,7 @@ class CharacterTypeRepository extends BaseRepository { async getFirst() { try { const repository = this.em.getRepository(CharacterType) - return await repository.findOne({ id: { $exists: true } }, { populate: ['*'] }) + return await repository.findOne({ id: { $exists: true } }) } catch (error: any) { appLogger.error(`Failed to get first character type: ${error instanceof Error ? error.message : String(error)}`) return null @@ -16,7 +16,7 @@ class CharacterTypeRepository extends BaseRepository { async getAll() { try { const repository = this.em.getRepository(CharacterType) - return await repository.findAll({ populate: ['*'] }) + return await repository.findAll() } catch (error: any) { appLogger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`) return null @@ -26,7 +26,7 @@ class CharacterTypeRepository extends BaseRepository { async getById(id: number) { try { const repository = this.em.getRepository(CharacterType) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { appLogger.error(`Failed to get character type by ID: ${error instanceof Error ? error.message : String(error)}`) return null diff --git a/src/repositories/chatRepository.ts b/src/repositories/chatRepository.ts index 80c3051..3e2b169 100644 --- a/src/repositories/chatRepository.ts +++ b/src/repositories/chatRepository.ts @@ -6,12 +6,9 @@ class ChatRepository extends BaseRepository { async getById(id: number): Promise { try { const repository = this.em.getRepository(Chat) - return await repository.find( - { - id - }, - { populate: ['*'] } - ) + return await repository.find({ + id + }) } catch (error: any) { appLogger.error(`Failed to get chat by ID: ${error instanceof Error ? error.message : String(error)}`) return [] @@ -21,7 +18,7 @@ class ChatRepository extends BaseRepository { async getAll(): Promise { try { const repository = this.em.getRepository(Chat) - return await repository.findAll({ populate: ['*'] }) + return await repository.findAll() } catch (error: any) { appLogger.error(`Failed to get all chats: ${error instanceof Error ? error.message : String(error)}`) return [] @@ -31,7 +28,7 @@ class ChatRepository extends BaseRepository { async getByCharacterId(characterId: number): Promise { try { const repository = this.em.getRepository(Chat) - return await repository.find({ character: characterId }, { populate: ['*'] }) + return await repository.find({ character: characterId }) } catch (error: any) { appLogger.error(`Failed to get chats by character ID: ${error instanceof Error ? error.message : String(error)}`) return [] @@ -41,7 +38,7 @@ class ChatRepository extends BaseRepository { async getByZoneId(zoneId: number): Promise { try { const repository = this.em.getRepository(Chat) - return await repository.find({ zone: zoneId }, { populate: ['*'] }) + return await repository.find({ zone: zoneId }) } catch (error: any) { appLogger.error(`Failed to get chats by zone ID: ${error instanceof Error ? error.message : String(error)}`) return [] diff --git a/src/repositories/spriteRepository.ts b/src/repositories/spriteRepository.ts index 2d6c333..e748eba 100644 --- a/src/repositories/spriteRepository.ts +++ b/src/repositories/spriteRepository.ts @@ -7,7 +7,7 @@ class SpriteRepository extends BaseRepository { async getById(id: FilterValue<`${string}-${string}-${string}-${string}-${string}`>) { try { const repository = this.em.getRepository(Sprite) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { return null } @@ -16,7 +16,7 @@ class SpriteRepository extends BaseRepository { async getAll(): Promise { try { const repository = this.em.getRepository(Sprite) - return await repository.findAll({ populate: ['*'] }) + return await repository.findAll() } catch (error: any) { return null } diff --git a/src/repositories/tileRepository.ts b/src/repositories/tileRepository.ts index a874395..a4b4179 100644 --- a/src/repositories/tileRepository.ts +++ b/src/repositories/tileRepository.ts @@ -2,9 +2,9 @@ import { FilterValue } from '@mikro-orm/core' import { BaseRepository } from '#application/base/baseRepository' import { unduplicateArray } from '#application/utilities' -import { FlattenZoneArray } from '#application/zone' import { Tile } from '#entities/tile' import { Zone } from '#entities/zone' +import ZoneService from '#services/zoneService' class TileRepository extends BaseRepository { async getById(id: FilterValue<`${string}-${string}-${string}-${string}-${string}`>): Promise { @@ -44,7 +44,7 @@ class TileRepository extends BaseRepository { const zone = await repository.findOne({ id: zoneId }) if (!zone) return null - const zoneTileArray = unduplicateArray(FlattenZoneArray(JSON.parse(JSON.stringify(zone.tiles)))) + const zoneTileArray = unduplicateArray(ZoneService.flattenZoneArray(JSON.parse(JSON.stringify(zone.tiles)))) return await tileRepository.find({ id: zoneTileArray diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 8725822..ff2828e 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -6,7 +6,7 @@ class UserRepository extends BaseRepository { async getById(id: number) { try { const repository = this.em.getRepository(User) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { appLogger.error(`Failed to get user by ID: ${error instanceof Error ? error.message : String(error)}`) return null @@ -16,7 +16,7 @@ class UserRepository extends BaseRepository { async getByUsername(username: string) { try { const repository = this.em.getRepository(User) - return await repository.findOne({ username }, { populate: ['*'] }) + return await repository.findOne({ username }) } catch (error: any) { appLogger.error(`Failed to get user by username: ${error instanceof Error ? error.message : String(error)}`) return null @@ -26,7 +26,7 @@ class UserRepository extends BaseRepository { async getByEmail(email: string) { try { const repository = this.em.getRepository(User) - return await repository.findOne({ email }, { populate: ['*'] }) + return await repository.findOne({ email }) } catch (error: any) { appLogger.error(`Failed to get user by email: ${error instanceof Error ? error.message : String(error)}`) return null diff --git a/src/repositories/zoneRepository.ts b/src/repositories/zoneRepository.ts index 3f0b270..17ae904 100644 --- a/src/repositories/zoneRepository.ts +++ b/src/repositories/zoneRepository.ts @@ -8,7 +8,7 @@ class ZoneRepository extends BaseRepository { async getFirst(): Promise { try { const repository = this.em.getRepository(Zone) - return await repository.findOne({ id: { $exists: true } }, { populate: ['*'] }) + return await repository.findOne({ id: { $exists: true } }) } catch (error: any) { appLogger.error(`Failed to get first zone: ${error instanceof Error ? error.message : String(error)}`) return null @@ -18,7 +18,7 @@ class ZoneRepository extends BaseRepository { async getAll(): Promise { try { const repository = this.em.getRepository(Zone) - return await repository.findAll({ populate: ['*'] }) + return await repository.findAll() } catch (error: any) { appLogger.error(`Failed to get all zone: ${error.message}`) return [] @@ -28,7 +28,7 @@ class ZoneRepository extends BaseRepository { async getById(id: number) { try { const repository = this.em.getRepository(Zone) - return await repository.findOne({ id }, { populate: ['*'] }) + return await repository.findOne({ id }) } catch (error: any) { appLogger.error(`Failed to get zone by id: ${error.message}`) return null @@ -38,7 +38,7 @@ class ZoneRepository extends BaseRepository { async getEventTiles(id: number): Promise { try { const repository = this.em.getRepository(ZoneEventTile) - return await repository.find({ zone: id }, { populate: ['*'] }) + return await repository.find({ zone: id }) } catch (error: any) { appLogger.error(`Failed to get zone event tiles: ${error.message}`) return [] @@ -48,14 +48,11 @@ class ZoneRepository extends BaseRepository { async getFirstEventTile(zoneId: number, positionX: number, positionY: number): Promise { try { const repository = this.em.getRepository(ZoneEventTile) - return await repository.findOne( - { - zone: zoneId, - positionX: positionX, - positionY: positionY - }, - { populate: ['*'] } - ) + return await repository.findOne({ + zone: zoneId, + positionX: positionX, + positionY: positionY + }) } catch (error: any) { appLogger.error(`Failed to get zone event tile: ${error.message}`) return null @@ -65,7 +62,7 @@ class ZoneRepository extends BaseRepository { async getZoneObjects(id: number): Promise { try { const repository = this.em.getRepository(ZoneObject) - return await repository.find({ zone: id }, { populate: ['*'] }) + return await repository.find({ zone: id }) } catch (error: any) { appLogger.error(`Failed to get zone objects: ${error.message}`) return [] diff --git a/src/server.ts b/src/server.ts index 5eb58de..83cb496 100644 --- a/src/server.ts +++ b/src/server.ts @@ -145,4 +145,4 @@ export class Server { // Start the server const server = new Server() -server.start() \ No newline at end of file +server.start() diff --git a/src/services/characterService.ts b/src/services/characterService.ts index e16ea66..4d32c60 100644 --- a/src/services/characterService.ts +++ b/src/services/characterService.ts @@ -1,23 +1,28 @@ -import { AStar } from '#application/character/aStar' -import Rotation from '#application/character/rotation' -import { appLogger, gameLogger } from '#application/logger' +import config from '#application/config' +import { gameLogger } from '#application/logger' import { Character } from '#entities/character' import { Zone } from '#entities/zone' import ZoneManager from '#managers/zoneManager' -import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterRepository from '#repositories/characterRepository' -import UserRepository from '#repositories/userRepository' import ZoneRepository from '#repositories/zoneRepository' -interface Position { - x: number - y: number -} +type Position = { x: number; y: number } +export type Node = Position & { parent?: Node; g: number; h: number; f: number } export class CharacterService { private readonly MOVEMENT_DELAY_MS = 250 + private readonly DIRECTIONS = [ + { x: 0, y: -1 }, // Up + { x: 0, y: 1 }, // Down + { x: -1, y: 0 }, // Left + { x: 1, y: 0 }, // Right + { x: -1, y: -1 }, + { x: -1, y: 1 }, + { x: 1, y: -1 }, + { x: 1, y: 1 } + ] - async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) { + public async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) { const character = await CharacterRepository.getById(id) if (!character) return null @@ -51,14 +56,91 @@ export class CharacterService { y: Math.floor(targetY) } - return AStar.findPath(start, end, grid) + return this.findPath(start, end, grid) + } + + static calculateRotation(X1: number, Y1: number, X2: number, Y2: number): number { + if (config.ALLOW_DIAGONAL_MOVEMENT) { + // Check diagonal movements + if (X1 > X2 && Y1 > Y2) { + return 7 + } else if (X1 < X2 && Y1 < Y2) { + return 3 + } else if (X1 > X2 && Y1 < Y2) { + return 5 + } else if (X1 < X2 && Y1 > Y2) { + return 1 + } + } + + // Non-diagonal movements + if (X1 > X2) { + return 6 + } else if (X1 < X2) { + return 2 + } else if (Y1 < Y2) { + return 4 + } else if (Y1 > Y2) { + return 0 + } + + return 0 // Default case } public async applyMovementDelay(): Promise { await new Promise((resolve) => setTimeout(resolve, this.MOVEMENT_DELAY_MS)) } - private isValidPosition(position: Position): boolean { - return Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0 + private findPath(start: Position, end: Position, grid: number[][]): Node[] { + const openList: Node[] = [{ ...start, g: 0, h: 0, f: 0 }] + const closedSet = new Set() + const getKey = (p: Position) => `${p.x},${p.y}` + + while (openList.length > 0) { + const current = openList.reduce((min, node) => (node.f < min.f ? node : min)) + if (current.x === end.x && current.y === end.y) return this.reconstructPath(current) + + openList.splice(openList.indexOf(current), 1) + closedSet.add(getKey(current)) + + const neighbors = this.DIRECTIONS.slice(0, config.ALLOW_DIAGONAL_MOVEMENT ? 8 : 4) + .map((dir) => ({ x: current.x + dir.x, y: current.y + dir.y })) + .filter((pos) => this.isValidPosition(pos, grid, end)) + + for (const neighbor of neighbors) { + if (closedSet.has(getKey(neighbor))) continue + + const g = current.g + this.getDistance(current, neighbor) + const existing = openList.find((node) => node.x === neighbor.x && node.y === neighbor.y) + + if (!existing || g < existing.g) { + const h = this.getDistance(neighbor, end) + const node: Node = { ...neighbor, g, h, f: g + h, parent: current } + if (!existing) openList.push(node) + else Object.assign(existing, node) + } + } + } + + return [] // No path found + } + + private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { + return pos.x >= 0 && pos.y >= 0 && pos.x < grid[0].length && pos.y < grid.length && (grid[pos.y][pos.x] === 0 || (pos.x === end.x && pos.y === end.y)) + } + + private getDistance(a: Position, b: Position): number { + const dx = Math.abs(a.x - b.x), + dy = Math.abs(a.y - b.y) + // Manhattan distance for straight paths, then Euclidean for diagonals + return dx + dy + (Math.sqrt(2) - 2) * Math.min(dx, dy) + } + + private reconstructPath(endNode: Node): Node[] { + const path: Node[] = [] + for (let current: Node | undefined = endNode; current; current = current.parent) { + path.unshift(current) + } + return path } } diff --git a/src/services/chatService.ts b/src/services/chatService.ts index 862b59a..c613e62 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -32,6 +32,18 @@ class ChatService { return false } } + + public isCommand(message: string, command?: string) { + if (command) { + return message === `/${command}` || message.startsWith(`/${command} `) + } + return message.startsWith('/') + } + + public getArgs(command: string, message: string): string[] | undefined { + if (!this.isCommand(message, command)) return + return message.split(`/${command} `)[1].split(' ') + } } export default ChatService diff --git a/src/services/zoneService.ts b/src/services/zoneService.ts index e7fb729..3fc02d5 100644 --- a/src/services/zoneService.ts +++ b/src/services/zoneService.ts @@ -1,3 +1,13 @@ -class ZoneService {} +class ZoneService { + public flattenZoneArray(tiles: string[][]) { + const normalArray = [] + + for (const row of tiles) { + normalArray.push(...row) + } + + return normalArray + } +} export default ZoneService diff --git a/src/socketEvents/character/charactersScreen/character_hair_list.ts b/src/socketEvents/character/charactersScreen/characterHairList.ts similarity index 70% rename from src/socketEvents/character/charactersScreen/character_hair_list.ts rename to src/socketEvents/character/charactersScreen/characterHairList.ts index 10bfe66..2aa2709 100644 --- a/src/socketEvents/character/charactersScreen/character_hair_list.ts +++ b/src/socketEvents/character/charactersScreen/characterHairList.ts @@ -1,5 +1,6 @@ import { Server } from 'socket.io' +import Database from '#application/database' import { TSocket } from '#application/types' import { CharacterHair } from '#entities/characterHair' import characterHairRepository from '#repositories/characterHairRepository' @@ -16,8 +17,9 @@ export default class characterHairListEvent { this.socket.on('character:hair:list', this.handleEvent.bind(this)) } - private async handleEvent(data: IPayload, callback: (response: CharacterHair[] | null) => void): Promise { - const items = await characterHairRepository.getAllSelectable() + private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise { + const items: CharacterHair[] = await characterHairRepository.getAllSelectable() + await Database.getEntityManager().populate(items, ['sprite']) callback(items) } } diff --git a/src/socketEvents/character/connect.ts b/src/socketEvents/character/connect.ts index 53e10fd..c75499d 100644 --- a/src/socketEvents/character/connect.ts +++ b/src/socketEvents/character/connect.ts @@ -1,5 +1,6 @@ import { Server } from 'socket.io' +import Database from '#application/database' import { gameLogger } from '#application/logger' import { TSocket } from '#application/types' import ZoneManager from '#managers/zoneManager' @@ -45,7 +46,7 @@ export default class CharacterConnectEvent { // Set character hair const characterHair = await CharacterHairRepository.getById(characterHairId ?? 0) - await character.setCharacterHair(characterHair).save() + await character.setCharacterHair(characterHair).update() // Emit character connect event this.socket.emit('character:connect', character) diff --git a/src/socketEvents/character/create.ts b/src/socketEvents/character/create.ts index 9fffa6e..81c39fa 100644 --- a/src/socketEvents/character/create.ts +++ b/src/socketEvents/character/create.ts @@ -36,17 +36,14 @@ export default class CharacterCreateEvent { return this.socket.emit('notification', { message: 'Character name already exists' }) } - let characters: Character[] = (await CharacterRepository.getByUserId(user.getId())) + let characters: Character[] = await CharacterRepository.getByUserId(user.getId()) if (characters.length >= 4) { return this.socket.emit('notification', { message: 'You can only have 4 characters' }) } const newCharacter = new Character() - await newCharacter - .setName(data.name) - .setUser(user) - .save() + await newCharacter.setName(data.name).setUser(user).save() if (!newCharacter) return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' }) diff --git a/src/socketEvents/character/delete.ts b/src/socketEvents/character/delete.ts index 73a89ed..f6723ac 100644 --- a/src/socketEvents/character/delete.ts +++ b/src/socketEvents/character/delete.ts @@ -32,7 +32,7 @@ export default class CharacterDeleteEvent { await character.delete() } - const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) + const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) this.socket.emit('character:list', characters) } catch (error: any) { diff --git a/src/socketEvents/character/list.ts b/src/socketEvents/character/list.ts index d307866..c74247c 100644 --- a/src/socketEvents/character/list.ts +++ b/src/socketEvents/character/list.ts @@ -1,5 +1,6 @@ import { Socket, Server } from 'socket.io' +import Database from '#application/database' import { gameLogger } from '#application/logger' import { TSocket } from '#application/types' import { Character } from '#entities/character' @@ -12,12 +13,14 @@ export default class CharacterListEvent { ) {} public listen(): void { - this.socket.on('character:list', this.handleCharacterList.bind(this)) + this.socket.on('character:list', this.handleEvent.bind(this)) } - private async handleCharacterList(data: any): Promise { + private async handleEvent(data: any): Promise { try { const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) + await Database.getEntityManager().populate(characters, ['characterType', 'characterHair']) + this.socket.emit('character:list', characters) } catch (error: any) { gameLogger.error('character:list error', error.message) diff --git a/src/socketEvents/gameMaster/assetManager/object/upload.ts b/src/socketEvents/gameMaster/assetManager/object/upload.ts index 1fd1861..3bb07f0 100644 --- a/src/socketEvents/gameMaster/assetManager/object/upload.ts +++ b/src/socketEvents/gameMaster/assetManager/object/upload.ts @@ -1,7 +1,6 @@ import fs from 'fs/promises' import { writeFile } from 'node:fs/promises' - import sharp from 'sharp' import { Server } from 'socket.io' diff --git a/src/socketEvents/gameMaster/assetManager/sprite/copy.ts b/src/socketEvents/gameMaster/assetManager/sprite/copy.ts index d756c3a..54b7b9c 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/copy.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/copy.ts @@ -7,7 +7,6 @@ import prisma from '#application/prisma' import { TSocket } from '#application/types' import CharacterRepository from '#repositories/characterRepository' - interface CopyPayload { id: string } diff --git a/src/socketEvents/zone/characterJoin.ts b/src/socketEvents/zone/characterJoin.ts index b1d8bdf..8e3fa0a 100644 --- a/src/socketEvents/zone/characterJoin.ts +++ b/src/socketEvents/zone/characterJoin.ts @@ -1,5 +1,7 @@ import { Server } from 'socket.io' +import { BaseEvent } from '#application/base/baseEvent' +import Database from '#application/database' import { gameLogger } from '#application/logger' import { TSocket } from '#application/types' import { Zone } from '#entities/zone' @@ -14,17 +16,12 @@ interface IResponse { characters: zoneCharacter[] } -export default class CharacterJoinEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - +export default class CharacterJoinEvent extends BaseEvent { public listen(): void { - this.socket.on('zone:character:join', this.handleCharacterJoin.bind(this)) + this.socket.on('zone:character:join', this.handleEvent.bind(this)) } - private async handleCharacterJoin(callback: (response: IResponse) => void): Promise { + private async handleEvent(callback: (response: IResponse) => void): Promise { try { if (!this.socket.characterId) { gameLogger.error('zone:character:join error', 'Zone requested but no character id set') diff --git a/src/socketEvents/zone/characterLeave.ts b/src/socketEvents/zone/characterLeave.ts index 5f13eb8..98a84f7 100644 --- a/src/socketEvents/zone/characterLeave.ts +++ b/src/socketEvents/zone/characterLeave.ts @@ -7,22 +7,25 @@ import CharacterRepository from '#repositories/characterRepository' import ZoneRepository from '#repositories/zoneRepository' export default class ZoneLeaveEvent { - constructor(private readonly io: Server, private readonly socket: TSocket) {} + constructor( + private readonly io: Server, + private readonly socket: TSocket + ) {} public listen(): void { - this.socket.on('zone:character:leave', this.handleZoneLeave.bind(this)) + this.socket.on('zone:character:leave', this.handleEvent.bind(this)) } - private async handleZoneLeave(): Promise { + private async handleEvent(): Promise { try { if (!this.socket.characterId) { - gameLogger.error('zone:character:join error', 'Zone requested but no character id set') + gameLogger.error('zone:character:leave error', 'Zone requested but no character id set') return } const character = await CharacterRepository.getById(this.socket.characterId) if (!character) { - gameLogger.error('zone:character:join error', 'Character not found') + gameLogger.error('zone:character:leave error', 'Character not found') return } @@ -31,13 +34,13 @@ export default class ZoneLeaveEvent { */ const zone = character.zone if (!zone) { - gameLogger.error('zone:character:join error', 'Zone not found') + gameLogger.error('zone:character:leave error', 'Zone not found') return } const loadedZone = ZoneManager.getZoneById(zone.id) if (!loadedZone) { - gameLogger.error('zone:character:join error', 'Loaded zone not found') + gameLogger.error('zone:character:leave error', 'Loaded zone not found') return } diff --git a/src/socketEvents/zone/characterMove.ts b/src/socketEvents/zone/characterMove.ts index 0524380..1d71e6e 100644 --- a/src/socketEvents/zone/characterMove.ts +++ b/src/socketEvents/zone/characterMove.ts @@ -1,6 +1,5 @@ import { Server } from 'socket.io' -import Rotation from '#application/character/rotation' import { gameLogger } from '#application/logger' import { TSocket, ZoneEventTileWithTeleport } from '#application/types' import ZoneManager from '#managers/zoneManager' @@ -19,10 +18,10 @@ export default class CharacterMove { ) {} public listen(): void { - this.socket.on('character:move', this.handleCharacterMove.bind(this)) + this.socket.on('character:move', this.handleEvent.bind(this)) } - private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise { + private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise { const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) if (!zoneCharacter?.character) { gameLogger.error('character:move error', 'Character not found or not initialized') @@ -56,7 +55,7 @@ export default class CharacterMove { } const [start, end] = [path[i], path[i + 1]] - character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y) + character.rotation = CharacterService.calculateRotation(start.x, start.y, end.x, end.y) const zoneEventTile = await zoneEventTileRepository.getEventTileByZoneIdAndPosition(character.zone!.id, Math.floor(end.x), Math.floor(end.y))