Renamed folder utilities > application, added baseEntity class, updated baseRepo class, removed prisma helper
This commit is contained in:
67
src/application/bases/baseEntity.ts
Normal file
67
src/application/bases/baseEntity.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
24
src/application/bases/baseRepository.ts
Normal file
24
src/application/bases/baseRepository.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
70
src/application/character/aStar.ts
Normal file
70
src/application/character/aStar.ts
Normal 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
|
||||
}
|
||||
}
|
33
src/application/character/rotation.ts
Normal file
33
src/application/character/rotation.ts
Normal 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
11
src/application/chat.ts
Normal 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
37
src/application/config.ts
Normal 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
|
27
src/application/database.ts
Normal file
27
src/application/database.ts
Normal 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
47
src/application/enums.ts
Normal 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
66
src/application/logger.ts
Normal 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 }
|
33
src/application/storage.ts
Normal file
33
src/application/storage.ts
Normal 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
52
src/application/types.ts
Normal 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
|
||||
// }
|
3
src/application/utilities.ts
Normal file
3
src/application/utilities.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function unduplicateArray(array: any[]) {
|
||||
return [...new Set(array.flat())]
|
||||
}
|
60
src/application/zodTypes.ts
Normal file
60
src/application/zodTypes.ts
Normal 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
9
src/application/zone.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function FlattenZoneArray(tiles: string[][]) {
|
||||
const normalArray = []
|
||||
|
||||
for (const row of tiles) {
|
||||
normalArray.push(...row)
|
||||
}
|
||||
|
||||
return normalArray
|
||||
}
|
Reference in New Issue
Block a user