Greatly improved server code base

This commit is contained in:
Dennis Postma 2024-12-28 17:26:17 +01:00
parent bd85908014
commit bd3bf6f580
39 changed files with 227 additions and 246 deletions

View File

@ -0,0 +1,5 @@
import { Server } from 'socket.io'
export abstract class BaseCommand {
constructor(readonly io: Server) {}
}

View File

@ -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
}
}
}
}

View File

@ -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
) {}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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(' ')
}

View File

@ -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

View File

@ -41,7 +41,7 @@ export type AssetData = {
frameRate?: number
frameWidth?: number
frameHeight?: number
frameRate?: number
frameCount?: number
}
export type WorldSettings = {

View File

@ -1,9 +0,0 @@
export function FlattenZoneArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}

View File

@ -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')

View File

@ -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()

View File

@ -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())
}

View File

@ -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')

View File

@ -6,7 +6,6 @@ import { PasswordResetToken } from './passwordResetToken'
import { BaseEntity } from '#application/base/baseEntity'
@Entity()
export class User extends BaseEntity {
@PrimaryKey()

View File

@ -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

View File

@ -34,6 +34,7 @@ class LoadedZone {
}
public getCharactersInZone(): ZoneCharacter[] {
console.log(this.characters)
return this.characters
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 []

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 []

View File

@ -145,4 +145,4 @@ export class Server {
// Start the server
const server = new Server()
server.start()
server.start()

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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).' })

View File

@ -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) {

View File

@ -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)

View File

@ -1,7 +1,6 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import { Server } from 'socket.io'

View File

@ -7,7 +7,6 @@ import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
interface CopyPayload {
id: string
}

View File

@ -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')

View File

@ -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
}

View File

@ -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))