Greatly improved server code base
This commit is contained in:
parent
bd85908014
commit
bd3bf6f580
5
src/application/base/baseCommand.ts
Normal file
5
src/application/base/baseCommand.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
export abstract class BaseCommand {
|
||||
constructor(readonly io: Server) {}
|
||||
}
|
@ -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<this> {
|
||||
return this.performDbOperation('persist', 'save entity')
|
||||
return this.execute('persist', 'save entity')
|
||||
}
|
||||
|
||||
async update(): Promise<this> {
|
||||
return this.performDbOperation('merge', 'update entity')
|
||||
return this.execute('merge', 'update entity')
|
||||
}
|
||||
|
||||
async delete(): Promise<this> {
|
||||
return this.performDbOperation('remove', 'remove entity')
|
||||
return this.execute('remove', 'remove entity')
|
||||
}
|
||||
|
||||
private async performDbOperation(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise<this> {
|
||||
private async execute(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise<this> {
|
||||
try {
|
||||
const em = this.getEntityManager()
|
||||
|
||||
@ -38,4 +39,4 @@ export abstract class BaseEntity {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
|
||||
export abstract class BaseEvent {
|
||||
|
||||
}
|
||||
constructor(
|
||||
readonly io: Server,
|
||||
readonly socket: TSocket
|
||||
) {}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<string>()
|
||||
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
|
||||
}
|
||||
}
|
@ -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
|
@ -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(' ')
|
||||
}
|
@ -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
|
||||
export default Database
|
||||
|
@ -41,7 +41,7 @@ export type AssetData = {
|
||||
frameRate?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
frameRate?: number
|
||||
frameCount?: number
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
|
@ -1,9 +0,0 @@
|
||||
export function FlattenZoneArray(tiles: string[][]) {
|
||||
const normalArray = []
|
||||
|
||||
for (const row of tiles) {
|
||||
normalArray.push(...row)
|
||||
}
|
||||
|
||||
return normalArray
|
||||
}
|
@ -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')
|
||||
|
@ -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<void> {
|
||||
// Assets
|
||||
await this.importTiles()
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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<void> {
|
||||
// Get all tiles
|
||||
const tilesDir = getPublicPath('tiles')
|
||||
|
@ -6,7 +6,6 @@ import { PasswordResetToken } from './passwordResetToken'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
|
@ -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
|
||||
|
@ -34,6 +34,7 @@ class LoadedZone {
|
||||
}
|
||||
|
||||
public getCharactersInZone(): ZoneCharacter[] {
|
||||
console.log(this.characters)
|
||||
return this.characters
|
||||
}
|
||||
|
||||
|
@ -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<CharacterHair[]> {
|
||||
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<CharacterHair[]> {
|
||||
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<CharacterHair | null> {
|
||||
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
|
||||
|
@ -6,7 +6,7 @@ class CharacterRepository extends BaseRepository {
|
||||
async getByUserId(userId: number): Promise<Character[]> {
|
||||
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<Character | null> {
|
||||
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<Character | null> {
|
||||
async getById(id: number, populate?: string[]): Promise<Character | null> {
|
||||
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<Character | null> {
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -6,12 +6,9 @@ class ChatRepository extends BaseRepository {
|
||||
async getById(id: number): Promise<Chat[]> {
|
||||
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<Chat[]> {
|
||||
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<Chat[]> {
|
||||
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<Chat[]> {
|
||||
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 []
|
||||
|
@ -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<any> {
|
||||
try {
|
||||
const repository = this.em.getRepository(Sprite)
|
||||
return await repository.findAll({ populate: ['*'] })
|
||||
return await repository.findAll()
|
||||
} catch (error: any) {
|
||||
return null
|
||||
}
|
||||
|
@ -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<any> {
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -8,7 +8,7 @@ class ZoneRepository extends BaseRepository {
|
||||
async getFirst(): Promise<Zone | null> {
|
||||
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<Zone[]> {
|
||||
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<ZoneEventTile[]> {
|
||||
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<ZoneEventTile | null> {
|
||||
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<ZoneObject[]> {
|
||||
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 []
|
||||
|
@ -145,4 +145,4 @@ export class Server {
|
||||
|
||||
// Start the server
|
||||
const server = new Server()
|
||||
server.start()
|
||||
server.start()
|
||||
|
@ -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<void> {
|
||||
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<string>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<void> {
|
||||
const items = await characterHairRepository.getAllSelectable()
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
|
||||
await Database.getEntityManager().populate(items, ['sprite'])
|
||||
callback(items)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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).' })
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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<void> {
|
||||
private async handleEvent(data: any): Promise<void> {
|
||||
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)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import fs from 'fs/promises'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
|
||||
import sharp from 'sharp'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
|
@ -7,7 +7,6 @@ import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
|
||||
interface CopyPayload {
|
||||
id: string
|
||||
}
|
||||
|
@ -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<void> {
|
||||
private async handleEvent(callback: (response: IResponse) => void): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.characterId) {
|
||||
gameLogger.error('zone:character:join error', 'Zone requested but no character id set')
|
||||
|
@ -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<void> {
|
||||
private async handleEvent(): Promise<void> {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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<void> {
|
||||
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
||||
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))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user