Renamed folder utilities > application, added baseEntity class, updated baseRepo class, removed prisma helper

This commit is contained in:
2024-12-25 16:50:01 +01:00
parent f5a7a348e0
commit f4746722af
120 changed files with 423 additions and 378 deletions

View File

@ -0,0 +1,67 @@
import { Database } from '#application/database'
import { appLogger } from '#application/logger'
export abstract class BaseEntity {
async save(): Promise<this> {
try {
const orm = await Database.getInstance()
const em = orm.em.fork()
await em.begin()
try {
em.persist(this)
await em.flush()
await em.commit()
return this
} catch (error) {
await em.rollback()
throw error
}
} catch (error) {
appLogger.error(`Failed to save entity: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
async update(): Promise<this> {
try {
const orm = await Database.getInstance()
const em = orm.em.fork()
await em.begin()
try {
em.merge(this)
await em.flush()
await em.commit()
return this
} catch (error) {
await em.rollback()
throw error
}
} catch (error) {
appLogger.error(`Failed to update entity: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
async delete(): Promise<this> {
try {
const orm = await Database.getInstance()
const em = orm.em.fork()
await em.begin()
try {
em.remove(this)
await em.flush()
await em.commit()
return this
} catch (error) {
await em.rollback()
throw error
}
} catch (error) {
appLogger.error(`Failed to remove entity: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
}

View File

@ -0,0 +1,24 @@
import { appLogger } from '../logger'
import { Database } from '../database'
import { EntityManager, MikroORM } from '@mikro-orm/core'
export abstract class BaseRepository {
protected orm!: MikroORM
protected em!: EntityManager
constructor() {
this.initializeORM().catch((error) => {
appLogger.error(`Failed to initialize Repository: ${error instanceof Error ? error.message : String(error)}`)
})
}
private async initializeORM() {
try {
this.orm = await Database.getInstance()
this.em = this.orm.em.fork()
} catch (error: any) {
appLogger.error(`Failed to initialize ORM: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
}

View File

@ -0,0 +1,70 @@
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

@ -0,0 +1,33 @@
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

11
src/application/chat.ts Normal file
View File

@ -0,0 +1,11 @@
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(' ')
}

37
src/application/config.ts Normal file
View File

@ -0,0 +1,37 @@
import dotenv from 'dotenv'
dotenv.config()
class config {
// Server configuration
static ENV: string = process.env.ENV || 'development'
static HOST: string = process.env.HOST || '0.0.0.0'
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://noxious.gg'
// Database configuration
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
static DATABASE_URL: string = process.env.DATABASE_URL || 'mysql://root@localhost:3306/game'
static DB_HOST: string = process.env.DB_HOST || 'localhost'
static DB_USER: string = process.env.DB_USER || 'root'
static DB_PASS: string = process.env.DB_PASS || ''
static DB_PORT: number = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306
static DB_NAME: string = process.env.DB_NAME || 'game'
// Game configuration
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
// Default character create values
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
// Email configuration
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@noxious.gg'
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
}
export default config

View File

@ -0,0 +1,27 @@
import config from '../../mikro-orm.config'
// import { MikroORM } from '@mikro-orm/mariadb'
import { MikroORM } from '@mikro-orm/mysql'
import { appLogger } from './logger'
/**
* Singleton class for initializing and managing the database connection
*/
export class Database {
private static instance: MikroORM | undefined
private static async init(): Promise<MikroORM> {
try {
return await MikroORM.init(config)
} catch (error) {
appLogger.error(`MikroORM connection failed: ${error}`)
throw error
}
}
public static async getInstance(): Promise<MikroORM> {
if (!Database.instance) {
Database.instance = await Database.init()
}
return Database.instance
}
}

47
src/application/enums.ts Normal file
View File

@ -0,0 +1,47 @@
export enum ItemType {
WEAPON = 'WEAPON',
HELMET = 'HELMET',
CHEST = 'CHEST',
LEGS = 'LEGS',
BOOTS = 'BOOTS',
GLOVES = 'GLOVES',
RING = 'RING',
NECKLACE = 'NECKLACE'
}
export enum ItemRarity {
COMMON = 'COMMON',
UNCOMMON = 'UNCOMMON',
RARE = 'RARE',
EPIC = 'EPIC',
LEGENDARY = 'LEGENDARY'
}
export enum CharacterGender {
MALE = 'MALE',
FEMALE = 'FEMALE'
}
export enum CharacterRace {
HUMAN = 'HUMAN',
ELF = 'ELF',
DWARF = 'DWARF',
ORC = 'ORC',
GOBLIN = 'GOBLIN'
}
export enum CharacterEquipmentSlotType {
HEAD = 'HEAD',
BODY = 'BODY',
ARMS = 'ARMS',
LEGS = 'LEGS',
NECK = 'NECK',
RING = 'RING'
}
export enum ZoneEventTileType {
BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT',
NPC = 'NPC',
ITEM = 'ITEM'
}

66
src/application/logger.ts Normal file
View File

@ -0,0 +1,66 @@
import pino from 'pino'
import fs from 'fs'
import { getRootPath } from './storage'
// Array of log types
const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue', 'command'] as const
type LogType = (typeof LOG_TYPES)[number]
const createLogger = (name: LogType) =>
pino({
level: process.env.LOG_LEVEL || 'debug',
transport: {
target: 'pino/file',
options: {
destination: `./logs/${name}.log`,
mkdir: true
}
},
formatters: {
level: (label) => {
return { level: label.toUpperCase() }
}
},
timestamp: pino.stdTimeFunctions.isoTime,
base: null
})
// Create logger instances
const loggers = Object.fromEntries(LOG_TYPES.map((type) => [type, createLogger(type)])) as Record<LogType, ReturnType<typeof createLogger>>
const watchLogs = () => {
LOG_TYPES.forEach((type) => {
const logFile = getRootPath('logs', `${type}.log`)
// Get initial file size
const stats = fs.statSync(logFile)
let lastPosition = stats.size
fs.watch(logFile, (eventType) => {
if (eventType !== 'change') {
return
}
fs.stat(logFile, (err, stats) => {
if (err) return
if (stats.size > lastPosition) {
const stream = fs.createReadStream(logFile, {
start: lastPosition,
end: stats.size
})
stream.on('data', (chunk) => {
console.log(`[${type}]\n${chunk.toString()}`)
})
lastPosition = stats.size
}
})
})
})
}
export const { http: httpLogger, game: gameLogger, gameMaster: gameMasterLogger, app: appLogger, queue: queueLogger, command: commandLogger } = loggers
export { watchLogs }

View File

@ -0,0 +1,33 @@
import config from './config'
import path from 'path'
import fs from 'fs'
export function getRootPath(folder: string, ...additionalSegments: string[]) {
return path.join(process.cwd(), folder, ...additionalSegments)
}
export function getAppPath(folder: string, ...additionalSegments: string[]) {
const baseDir = config.ENV === 'development' ? 'src' : 'dist'
return path.join(process.cwd(), baseDir, folder, ...additionalSegments)
}
export function getPublicPath(folder: string, ...additionalSegments: string[]) {
return path.join(process.cwd(), 'public', folder, ...additionalSegments)
}
export function doesPathExist(path: string) {
try {
fs.accessSync(path, fs.constants.F_OK)
return true
} catch (e) {
return false
}
}
export function createDir(path: string) {
try {
fs.mkdirSync(path, { recursive: true })
} catch (e) {
console.error(e)
}
}

52
src/application/types.ts Normal file
View File

@ -0,0 +1,52 @@
import { Socket } from 'socket.io'
import { Character, User, ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client'
export type TSocket = Socket & {
userId?: number
characterId?: number
handshake?: {
query?: {
token?: any
}
}
request?: {
headers?: {
cookie?: any
}
}
}
export type ExtendedCharacter = Character & {
isMoving?: boolean
resetMovement?: boolean
}
export type ZoneEventTileWithTeleport = ZoneEventTile & {
teleport: ZoneEventTileTeleport
}
export type AssetData = {
key: string
data: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
originX?: number
originY?: number
isAnimated?: boolean
frameCount?: number
frameWidth?: number
frameHeight?: number
frameRate?: number
}
export type WorldSettings = {
date: Date
isRainEnabled: boolean
isFogEnabled: boolean
fogDensity: number
}
// export type TCharacter = Socket & {
// user?: User
// character?: Character
// }

View File

@ -0,0 +1,3 @@
export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())]
}

View File

@ -0,0 +1,60 @@
import { z } from 'zod'
export const loginAccountSchema = z.object({
username: z
.string()
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters long'
})
.max(255)
})
export const registerAccountSchema = z.object({
username: z
.string()
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters long'
})
.max(255)
})
export const resetPasswordSchema = z.object({
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
})
export const newPasswordSchema = z.object({
urlToken: z.string().min(10, { message: 'Invalid request' }).max(255, { message: 'Invalid request' }),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters long'
})
.max(255)
})
export const ZCharacterCreate = z.object({
name: z
.string()
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' })
})

9
src/application/zone.ts Normal file
View File

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