Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

155 changed files with 4883 additions and 3036 deletions

17
.dockerignore Normal file
View File

@ -0,0 +1,17 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
.env.*
docker-compose*
*.md
coverage
.vscode
.idea
dist
build
temp

View File

@ -6,10 +6,10 @@ JWT_SECRET="secret"
CLIENT_URL="http://localhost:5173"
# Database configuration
REDIS_URL="redis://@127.0.0.1:6379/4"
DB_HOST="localhost"
DB_USER="root"
DB_PASS=""
REDIS_URL="redis://@redis:6379/4"
DB_HOST="mariadb"
DB_USER="mariadb"
DB_PASS="mariadb"
DB_PORT="3306"
DB_NAME="game"
@ -26,7 +26,3 @@ SMTP_HOST=my.directonline.io
SMTP_PORT=587
SMTP_USER=no-reply@noxious.gg
SMTP_PASSWORD=""
# SSL
#PUBLIC_KEY_PATH=
#PRIVATE_KEY_PATH=

View File

@ -1 +0,0 @@
migrations

View File

@ -4,8 +4,5 @@
"tabWidth": 2,
"singleQuote": true,
"printWidth": 200,
"trailingComma": "none",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy", "classProperties"],
"importOrderCaseSensitive": false
"trailingComma": "none"
}

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM node:lts-alpine
# Install packages
RUN apk update
RUN apk add --no-cache tmux coreutils
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
# Modify CMD to use tmux
CMD npx mikro-orm-esm migration:up && npm run start

View File

@ -5,7 +5,7 @@ This is the server for the Noxious game.
## Projects requirements
- NodeJS 20.x or higher
- MariaDB 11.x or higher
- MySQL 8.x or higher
- Redis 7.x or higher
## Installation
@ -14,7 +14,7 @@ This is the server for the Noxious game.
2. Install dependencies with `npm install`
3. Copy the `.env.example` file to `.env` and fill in the required variables
4. Extract assets.zip to the `public` folder
5. After MySQL and Redis are running, run `npm run mikro-orm migration:up` to create the database schema
5. After MySQL and Redis are running, run `npx mikro-orm-esm migration:up` to create the database schema
6. Run the server with `npm run dev`
7. Write `init` in the console to import default data and restart the server
8. Write `tiles` in the console to fix tile sizes
@ -39,15 +39,15 @@ MikroORM is used as the ORM for the server.
### Create init. migrations
Run `npm run mikro-orm migration:create --initial` to create a new initial migration.
Run `npx mikro-orm-esm migration:create --initial` to create a new initial migration.
### Create migrations
Run `npm run mikro-orm migration:create` to create a new migration. You do this when you want to add a new table or change an existing one.
Run `npx mikro-orm-esm migration:create` to create a new migration. You do this when you want to add a new table or change an existing one.
### Apply migrations
Run `npm run mikro-orm migration:up` to apply all pending migrations.
Run `npx mikro-orm-esm migration:up` to apply all pending migrations.
### Import default data

94
docker-compose.yml Normal file
View File

@ -0,0 +1,94 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT}:${PORT}"
environment:
- ENV=${ENV}
- HOST=${HOST}
- PORT=${PORT}
- JWT_SECRET=${JWT_SECRET}
- CLIENT_URL=${CLIENT_URL}
- REDIS_URL=${REDIS_URL}
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- ALLOW_DIAGONAL_MOVEMENT=${ALLOW_DIAGONAL_MOVEMENT}
- DEFAULT_CHARACTER_ZONE=${DEFAULT_CHARACTER_ZONE}
- DEFAULT_CHARACTER_POS_X=${DEFAULT_CHARACTER_POS_X}
- DEFAULT_CHARACTER_POS_Y=${DEFAULT_CHARACTER_POS_Y}
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
volumes:
- app-public:/user/src/app/public
- app-logs:/user/src/app/logs
depends_on:
- mariadb
- redis
restart: unless-stopped
networks:
- app-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${HOST}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=${PORT}"
- "traefik.http.routers.app.middlewares=websocket"
traefik:
image: traefik:v2.10
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- traefik_data:/data
- ./traefik.toml:/etc/traefik/traefik.toml
restart: unless-stopped
networks:
- app-network
mariadb:
image: mariadb:lts
environment:
- MARIADB_USER=${DB_USER}
- MARIADB_PASSWORD=${DB_PASS}
- MARIADB_DATABASE=${DB_NAME}
- MARIADB_RANDOM_ROOT_PASSWORD=yes
volumes:
- mariadb-data:/var/lib/mysql
ports:
- "${DB_PORT}:3306"
restart: unless-stopped
networks:
- app-network
command: [ 'mariadbd', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ]
redis:
image: redis:7.4.2-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
ports:
- "6379:6379"
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
app-public:
app-logs:
mariadb-data:
redis-data:
traefik_data:

42
eslint.config.js Normal file
View File

@ -0,0 +1,42 @@
import eslint from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
export default [
eslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2023,
},
},
plugins: {
'@typescript-eslint': tseslint,
'import': importPlugin,
},
rules: {
...tseslint.configs['recommended'].rules,
...tseslint.configs['recommended-requiring-type-checking'].rules,
'import/order': ['error', {
'groups': [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
'object',
'type'
],
'newlines-between': 'always',
'alphabetize': {
'order': 'asc',
'caseInsensitive': true
}
}]
}
}
];

59
etc/traefik/traefik.toml Normal file
View File

@ -0,0 +1,59 @@
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[providers.docker]
endpoint = "unix:///var/run/docker.sock"
exposedByDefault = false
[certificatesResolvers.le.acme]
email = "your-email@example.com"
storage = "/data/acme.json"
[certificatesResolvers.le.acme.tlsChallenge]
[api]
dashboard = true
[ping] # Health check
entryPoint = "websecure"
[http.routers.api]
rule = "PathPrefix(`/api`)"
service = "api"
entryPoints = ["websecure"]
[http.services.api.loadBalancer]
[[http.services.api.loadBalancer.servers]]
url = "http://app:${PORT}"
# Added for websocket
[http.services.app.loadBalancer]
sticky = true
[[http.services.app.loadBalancer.servers]]
url = "http://app:${PORT}"
# Added for websocket
[http.routers.app]
rule = "Host(`${HOST}`)"
entrypoints = ["websecure"]
service = "app"
[http.routers.app.tls]
certresolver = "le"
[http.routers.app.middlewares]
# Enable websockets
- "websocket"
[http.middlewares]
[http.middlewares.websocket.headers]
accessControlAllowHeaders = ["Origin", "Content-Type", "Accept", "Authorization"]
accessControlAllowMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
accessControlAllowOrigin = ["*"]
accessControlExposeHeaders = ["Content-Length", "Content-Range"]

3605
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,23 +5,36 @@
"start": "tsx src/server.ts",
"dev": "nodemon --exec tsx src/server.ts",
"format": "prettier --write src/",
"mikro-orm": "tsx ./node_modules/.bin/mikro-orm-esm"
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"imports": {
"#root/*": "./src/*.js",
"#application/*": "./src/application/*.js",
"#commands/*": "./src/commands/*.js",
"#entities/*": "./src/entities/*.js",
"#controllers/*": "./src/controllers/*.js",
"#jobs/*": "./src/jobs/*.js",
"#managers/*": "./src/managers/*.js",
"#middleware/*": "./src/middleware/*.js",
"#models/*": "./src/models/*.js",
"#repositories/*": "./src/repositories/*.js",
"#services/*": "./src/services/*.js",
"#events/*": "./src/events/*.js"
},
"dependencies": {
"@mikro-orm/cli": "^6.4.2",
"@mikro-orm/core": "^6.4.2",
"@mikro-orm/mariadb": "^6.4.2",
"@mikro-orm/migrations": "^6.4.2",
"@mikro-orm/mysql": "^6.4.2",
"@mikro-orm/reflection": "^6.4.2",
"@mikro-orm/cli": "^6.4.2",
"@types/ioredis": "^4.28.10",
"bcryptjs": "^2.4.3",
"bufferutil": "^4.0.9",
"bullmq": "^5.13.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"https": "^1.0.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
@ -29,18 +42,21 @@
"reflect-metadata": "^0.2.2",
"sharp": "^0.33.4",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.3",
"utf-8-validate": "^6.0.5",
"zod": "^3.23.8"
},
"devDependencies": {
"ts-node": "^10.9.2",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.14.11",
"@types/nodemailer": "^6.4.16",
"@typescript-eslint/eslint-plugin": "^8.18.2",
"@typescript-eslint/parser": "^8.18.2",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"nodemon": "^3.1.4",
"prettier": "^3.3.3",
"tsx": "^4.19.2"

Binary file not shown.

View File

@ -1,6 +1,7 @@
import Logger, { LoggerType } from '@/application/logger'
import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseCommand {
protected readonly logger = Logger.type(LoggerType.COMMAND)
constructor(readonly io: Server) {}

View File

@ -1,7 +1,9 @@
import fs from 'fs'
import Logger, { LoggerType } from '@/application/logger'
import type { Response } from 'express'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseController {
protected readonly logger = Logger.type(LoggerType.HTTP)

View File

@ -1,7 +1,8 @@
import Database from '@/application/database'
import Logger, { LoggerType } from '@/application/logger'
import { EntityManager } from '@mikro-orm/core'
import Database from '#application/database'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseEntity {
protected readonly logger = Logger.type(LoggerType.ENTITY)
protected entityManager?: EntityManager

View File

@ -1,43 +1,22 @@
import { SocketEvent } from '@/application/enums'
import Logger, { LoggerType } from '@/application/logger'
import type { TSocket } from '@/application/types'
import { Character } from '@/entities/character'
import MapManager from '@/managers/mapManager'
import type MapCharacter from '@/models/mapCharacter'
import CharacterRepository from '@/repositories/characterRepository'
import { Server } from 'socket.io'
import type { TSocket } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME)
private lastActionTimes: Map<string, number> = new Map()
constructor(
readonly io: Server,
readonly socket: TSocket
) {}
protected isThrottled(actionId: string, throttleTime: number): boolean {
const now = Date.now()
const lastActionTime = this.lastActionTimes.get(actionId) || 0
if (now - lastActionTime < throttleTime) {
return true
}
this.lastActionTimes.set(actionId, now)
return false
}
protected getMapCharacter(): MapCharacter | null {
if (!this.socket.characterId) return null
return MapManager.getCharacterById(this.socket.characterId)
}
protected async getCharacter(): Promise<Character | null> {
if (!this.socket.characterId) return null
const characterRepository = new CharacterRepository()
return characterRepository.getById(this.socket.characterId)
return characterRepository.getById(this.socket.characterId!)
}
protected async isCharacterGM(): Promise<boolean> {
@ -45,16 +24,15 @@ export abstract class BaseEvent {
return character?.getRole() === 'gm'
}
protected sendNotificationAndLog(message: string): void {
console.log(message)
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message })
this.logger.error('Base event error' + `Player ${this.socket.userId}: ${message}`)
protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`)
}
protected handleError(context: string, error: unknown): void {
console.log(error)
const errorMessage = error instanceof Error ? error.message : error && typeof error === 'object' && 'toString' in error ? error.toString() : String(error)
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message: `Server error occured. Please contact the server administrator.` })
this.logger.error('Base event error: ' + errorMessage)
this.socket.emit('notification', { title: 'Server message', message: `Server error occured. Please contact the server administrator.` })
this.logger.error('Base event error', errorMessage)
}
}

View File

@ -1,7 +1,8 @@
import Database from '@/application/database'
import Logger, { LoggerType } from '@/application/logger'
import { EntityManager } from '@mikro-orm/core'
import Database from '#application/database'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseRepository {
protected readonly logger = Logger.type(LoggerType.REPOSITORY)
private entityManager?: EntityManager

View File

@ -1,4 +1,4 @@
import Logger, { LoggerType } from '@/application/logger'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseService {
protected readonly logger = Logger.type(LoggerType.GAME)

View File

@ -6,7 +6,7 @@ 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) : 4000
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'
@ -31,10 +31,6 @@ class config {
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'
// SSL
static PUBLIC_KEY_PATH: string = process.env.PUBLIC_KEY_PATH || ''
static PRIVATE_KEY_PATH: string = process.env.PRIVATE_KEY_PATH || ''
}
export default config

View File

@ -1,9 +1,11 @@
import * as fs from 'fs'
import * as path from 'path'
import { pathToFileURL } from 'url'
import Logger, { LoggerType } from '@/application/logger'
import Storage from '@/application/storage'
import type { Command } from '@/application/types'
import type { Command } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
import Storage from '#application/storage'
export class CommandRegistry {
private readonly commands: Map<string, Command> = new Map()

View File

@ -1,6 +1,7 @@
import * as fs from 'fs'
import * as path from 'path'
import Logger, { LoggerType } from '@/application/logger'
import Logger, { LoggerType } from '#application/logger'
export class LogReader {
private logger = Logger.type(LoggerType.CONSOLE)

View File

@ -1,8 +1,7 @@
// import { MikroORM } from '@mikro-orm/mysql'
import { MikroORM } from '@mikro-orm/mysql'
import Logger, { LoggerType } from '@/application/logger'
import config from '@/root/mikro-orm.config'
import { MikroORM } from '@mikro-orm/mariadb'
import Logger, { LoggerType } from '#application/logger'
import config from '#root/mikro-orm.config'
class Database {
private static orm: MikroORM

View File

@ -1,59 +1,17 @@
export enum SocketEvent {
CONNECT_ERROR = 'connect_error',
RECONNECT_FAILED = 'reconnect_failed',
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
CHARACTER_CREATE = '49',
CHARACTER_DELETE = '48',
CHARACTER_LIST = '47',
GM_CHARACTERHAIR_CREATE = '46',
GM_CHARACTERHAIR_REMOVE = '45',
GM_CHARACTERHAIR_LIST = '44',
GM_CHARACTERHAIR_UPDATE = '43',
GM_CHARACTERTYPE_CREATE = '42',
GM_CHARACTERTYPE_REMOVE = '41',
GM_CHARACTERTYPE_LIST = '40',
GM_CHARACTERTYPE_UPDATE = '39',
GM_ITEM_CREATE = '38',
GM_ITEM_REMOVE = '37',
GM_ITEM_LIST = '36',
GM_ITEM_UPDATE = '35',
GM_MAPOBJECT_LIST = '34',
GM_MAPOBJECT_REMOVE = '33',
GM_MAPOBJECT_UPDATE = '32',
GM_MAPOBJECT_UPLOAD = '31',
GM_SPRITE_COPY = '30',
GM_SPRITE_CREATE = '29',
GM_SPRITE_DELETE = '28',
GM_SPRITE_LIST = '27',
GM_SPRITE_UPDATE = '26',
GM_TILE_DELETE = '25',
GM_TILE_LIST = '24',
GM_TILE_UPDATE = '23',
GM_TILE_UPLOAD = '22',
GM_MAP_CREATE = '21',
GM_MAP_DELETE = '20',
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = 'disconnect',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = 'connection',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',
MAP_CHARACTER_TELEPORT = '4',
MAP_CHARACTER_JOIN = '3',
MAP_CHARACTER_LEAVE = '2',
MAP_CHARACTER_MOVE = '1',
CHAT_MESSAGE = '0'
CHARACTER_CONNECT = 1,
CHARACTER_MOVE = 2,
CHARACTER_MOVE_ERROR = 3,
CHARACTER_TELEPORT = 4,
ZONE_CHARACTER_LEAVE = 5,
ZONE_CHARACTER_JOIN = 6,
ZONE_CHARACTER_LIST = 7,
ZONE_CHARACTER_DELETE = 8,
ZONE_CHARACTER_CREATE = 9,
ZONE_CHARACTER_UPDATE = 10,
ZONE_CHARACTER_HAIR_UPDATE = 11,
ZONE_CHARACTER_HAIR_LIST = 12,
ZONE_CHARACTER_TELEPORT = 13
}
export enum ItemType {

View File

@ -1,5 +1,4 @@
import pino from 'pino'
const logger = pino.pino
export enum LoggerType {

View File

@ -1,6 +1,7 @@
import fs from 'fs'
import path from 'path'
import config from '@/application/config'
import config from '#application/config'
class Storage {
private readonly baseDir: string
@ -8,7 +9,7 @@ class Storage {
constructor() {
this.rootDir = process.cwd()
this.baseDir = config.ENV === 'development' ? 'src' : 'src'
this.baseDir = config.ENV === 'development' ? 'src' : 'dist'
}
/**

View File

@ -1,8 +1,9 @@
import { Character } from '@/entities/character'
import { MapEventTile } from '@/entities/mapEventTile'
import { MapEventTileTeleport } from '@/entities/mapEventTileTeleport'
import { Server, Socket } from 'socket.io'
import { Character } from '#entities/character'
import { MapEventTile } from '#entities/mapEventTile'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
export type UUID = `${string}-${string}-${string}-${string}-${string}`
export type TSocket = Socket & {

View File

@ -58,16 +58,3 @@ export const ZCharacterCreate = z.object({
.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' })
})
export const ZCharacterConnect = z.object({
characterId: z.string(),
characterHairId: z.string().optional().nullable(),
newNickname: 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'
})
.optional()
})

View File

@ -1,13 +1,13 @@
import { BaseCommand } from '@/application/base/baseCommand'
import { SocketEvent } from '@/application/enums'
import { Server } from 'socket.io'
import { BaseCommand } from '#application/base/baseCommand'
type CommandInput = string[]
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')
this.io.emit(SocketEvent.NOTIFICATION, { message: message })
this.io.emit('notification', { message: message })
}
}

View File

@ -1,23 +1,26 @@
import fs from 'fs'
import { BaseCommand } from '@/application/base/baseCommand'
import { CharacterGender, CharacterRace } from '@/application/enums'
import Storage from '@/application/storage'
import type { UUID } from '@/application/types'
import { Character } from '@/entities/character'
import { CharacterHair } from '@/entities/characterHair'
import { CharacterType } from '@/entities/characterType'
import { Map } from '@/entities/map'
import { MapEffect } from '@/entities/mapEffect'
import { MapObject } from '@/entities/mapObject'
import { Sprite } from '@/entities/sprite'
import { SpriteAction } from '@/entities/spriteAction'
import { Tile } from '@/entities/tile'
import { User } from '@/entities/user'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import MapRepository from '@/repositories/mapRepository'
import sharp from 'sharp'
import type { UUID } from '#application/types'
import { BaseCommand } from '#application/base/baseCommand'
import { CharacterGender, CharacterRace } from '#application/enums'
import Storage from '#application/storage'
import { Character } from '#entities/character'
import { CharacterHair } from '#entities/characterHair'
import { CharacterType } from '#entities/characterType'
import { Map } from '#entities/map'
import { MapEffect } from '#entities/mapEffect'
import { MapObject } from '#entities/mapObject'
import { Sprite } from '#entities/sprite'
import { SpriteAction } from '#entities/spriteAction'
import { Tile } from '#entities/tile'
import { User } from '#entities/user'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapRepository from '#repositories/mapRepository'
// @TODO : Replace this with seeding
// https://mikro-orm.io/docs/seeding
@ -30,8 +33,7 @@ export default class InitCommand extends BaseCommand {
// Assets
await this.importTiles()
await this.importMapObjects()
await this.createMaleCharacterType()
// await this.createFemaleCharacterType()
await this.createCharacterType()
await this.createCharacterHair()
// await this.createCharacterEquipment()
@ -75,9 +77,9 @@ export default class InitCommand extends BaseCommand {
}
}
private async createMaleCharacterType(): Promise<void> {
private async createCharacterType(): Promise<void> {
const characterSprite = new Sprite()
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character')
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Character')
await characterSprite.save()
const idleRightDownAction = new SpriteAction()
@ -273,213 +275,7 @@ export default class InitCommand extends BaseCommand {
const characterType = new CharacterType()
await characterType
.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0')
.setName('Male character')
.setGender(CharacterGender.MALE)
.setRace(CharacterRace.HUMAN)
.setIsSelectable(true)
.setSprite(characterSprite)
.save()
}
private async createFemaleCharacterType(): Promise<void> {
const characterSprite = new Sprite()
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character')
await characterSprite.save()
const idleRightDownAction = new SpriteAction()
await idleRightDownAction
.setAction('idle_right_down')
.setSprites([
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAABeCAYAAAAwnXTzAAAHNklEQVR4nNVaTWgbRxT+LBtF2AiD5EoNBNbQiL1IIF9iXKgvqt1L8CE51gSfYhMKpjE1FIoPviuQW9OTMLmk0B5MTzaGYAhGJznIUIR8kCAQVthLhLBwDJZ7WL3RzOzs7qziSz8QaGdH882beX/zRiOFX19BF4nd7RtVu720NaI7xlgYopX8NGYK867360XnvQ5xIGFid/tmJT+NRCoNI2cilslifGpC6PPq9ywu6ydYL27fBJFGdMhmCvMC2WgyI3zGpyYQy2TxcuOJ57JrEQJAIpUGAMQyWc8+YUg9CUk6I2cGzUmA38R8Cb3QPbvA9Xndt4+flFpaqiIdR93VpoOhCMMQyPBd0rm1Z2hWawCAXrujPeh6cWc4wsv6CcxHj1E5OMRVqym8W13bxOrapus3q2ubOMzeH46QpJtbe4bKwaFr0DfvGy7SN+8bfkP6E9otC5f1EwCOPdKy0qB3f/w+kECb0F7aGikdN5iURs7EVauJXruDT0d/sn78d3qu/PCtJ2GglpKUzWqNkUZh4N+fngr9eP966WOm/kvKSWm3LNZOCjQ+NcE+hCBzCfQ09tLWyIu9MgAIitNrd9A9u3B9eu0O1os7nqFK27WVjhsCKe2n/JHNR4a2p1nJTzNSgjoYe0sXipBIgYG08gSA4KgfipAkminMM0WiTCBIMi1CPpehQEwwciYMDGLl88VZvOBCkhe5kpAn4gkAsDSje3bBvFAsk4WZyeJlzmSSl3bV+c2InCZSpAfAlksF3jZV/SoHhygdN1ySChLySRMAJOZmmSRy6mBmsoIHIkkBJ8o4W9BwTdS1pLRXiblZJzlCnbmquJln/Zw0Iwuj/8xndE6/11hRLC0zfD5pimWyGE1mADgZWTRlcCRg7TKojfrJiqaUMJoyBN/Ik6hyGR7Ul/ypkTOBvlt0SUgzikzGhZl2zy6YG6PBeL9JuKyfsHZgELxX8tNCBicQypomD+rlN2nwy/oJeu0OLusnsFuWMkd1LSktJy3PVauJysEhZgrzbC+pHRg4c/tgEL5Kxw283HiC8akJl7Z6ehqSjuxppjAvRAIikhWDj5sqxRoDRGMHgE7tmElROm5gJT8txEIALompPy+pCp4SknQE3ptEUwYik3EAYG4OAKIwXOPImsoIE6k0oimDLSUvHfshR0ZGDgDxZF+jOaJmtYavHroFYVoqayhpHg9eMnl/VPulOvQIZkGDXbWawubLkE/Aqna7ZaF7doFYJivYIiOUPYwurs/rLg/jB0FC1bIEkZF3Ic3mzYTP2pWEughzVJNXjRHS/hEoeKoSJR1i+t1oMiMopEvCXruj1FD+PSBqIC2dDNU4jHAYhfEbmHB9XhecuMvTBJkE0E/zuWfz0WPWHgRGGFZDdQanifOrp62lzWot8NzgBzL+iBwpvGC3LEbqRcxvB6/h9lGZ2aJrD5vVmpBPnqdNFD/2X378BLwvY+PuZwBiBKE8tfjxDpA2QWla5Y8dGDnT2ee9cj8ecp5ApTALCwvCc3F/X5jAAHewsLCAfXpP41WdPJZJqFNPS6cHk1peXvbsZ1nihAu//QL7qIza338NCPnIrcqYebL7950azL1791xkHz58cLXZR2Uk5madh72yW0v9pPUjo3bqQ6gcHKJ7dsFIGSG5Kjl+fQlIUylnFQgJcTOPubVnvgPJS/f27Vv2/d27dy7S9eIOVtc2YS9tjYwBjv10J+OIJ4G9n5+6jlmWZSGdTuP09JQtGZGenp4yUsuysL+/j6RVgy2R0vcxwLEhUluZLGnVQFoumwc/ITKFjbufUfJxxWOAYyu9dsez0kuGXuTsS9UnkUoHOv4xe2lrpLS7fTNTaALffO3ZMZFK43VhoMGUs1IqSYlx/WAPqoMogSmNnFnLZEbORDRlsA9fo6GcVgcRYKC+fikDDRiZjCMyGRcIwlT+BbPwShVY537GHTfziEzGlSdcbUKyF1Wpg5dgNJnxVC45EfMlJNIQkw1N5iLUQdAlya0TAsPfWQxFGEQWZPihCOUCwzAIfRXkRzQ+NaGsr/HQklBOEVUayZ+I/RBaQjL+LsT9so/KiGWClSkUIX+2l0tgzWoN8DljsDHCEFKqQAdRVdEhCKGXlNI9lfqrinkyQtuhLElYBx5KQgpJRg6uilOoeBgECr68OfCSye++mBAYaKjuwF9MeFu4NULdGsH/V0LdGkEoQr9BdTOBW5NQNwu4FUK6ZtCpcmj9gYfgt2y6JRVtCemSOSi1eL44i8Tu9o3XgVb7H0NEGgQjZ+L54qznKVrLees4Zr5PpmDgqtVEIpXGC+l2LVS0kK9+ZPAZQacWdw65fpddQaCjmpEzByUuriRG+3x9XkfczLNgzUNLwqtWU7yvePAdFh8uY+af1/0/6zQ8I73vlazyB/3SlZEDYpNOHeD6vC54nVv7qyAdx1fyAKoAuPteoOxbCfaC65ZbBb8iUdgj3n82KCKUHgxupgAAAABJRU5ErkJggg==',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(28)
.setFrameHeight(94)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const idleLeftUpAction = new SpriteAction()
await idleLeftUpAction
.setAction('idle_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(26)
.setFrameHeight(93)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const walkRightDownAction = new SpriteAction()
await walkRightDownAction
.setAction('walk_right_down')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 7,
y: 8
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 7,
y: 2
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAABcCAYAAAAPmrdOAAAHOklEQVR4nNVaTWgbRxT+/INi/ENANpINgTXYYi8StS81LjQX1+7Nh+RYU3xqTCgYYgiUUh986cmGXEpyE2mhpEcfbQRBEIKhIAUbipADEgSSXewlQrFxDJZ7kN9oZnZGO7uWC/1gsXa1mvfNe/Pe+2bXXXM/PUNYxLc3LlXXvcX1rrBj9UYxvDw1jum5u77vVzeb34chYkwgvr1xuTw1jngiCStjoy+VRv/IgHDPs6dpnJUPsLq5cWlKojuM8em5u4LxnuGUcPSPDKAvlcaTte+1YYpEAADiiSQAoC+V1t4ThUQgAZq9lbFNuQYSDUVAh9OjE1wcl9veY+KFUFmgItGPsu9aGFyLQBSDMoxCMLvyENX9EgCgUasbD766+bwzBM7KB7Dv3Uchl8e5WxW+e7DyGA9WHvt+82DlMfLpyc4QoNnPrjxEIZf3GXnxpuIj8eJNxWRoMwKe6+CsfACgWQ8oDGRk7LtvjA2GJuAtrndlixXmBStj49ytolGr4+Prv9h9/Gc6L3z7VSAB4ywgL1T3S4xEDBb++fEH4T6+P5y1LxMATEPAecFzHXadFmT/yAA7CKbpaVwJvcX1rq2dPQAQFmKjVsfp0YnvaNTqWN18HtiaQ5fibLEikKD1IB9yuuoQuhIuT40zEgS1OAmefSQCRAJoeUMmBJirokgEaMbTc3fZwiSlZDpzYwJ8OyVJxsPK2LDQ0gqPFmawxf0miIyWAC9AZYMAmCw7PTphVbIvlYadSuNJxmaeyW6314ddKllOKghoSTGdIuJrA4WBRyGXR7ZY0XrCR0BWv/HZGQDAp7cfAADdt4d8g/AVkpdidB0Atnb2lCSUdYA33jOcYobJeP/IAIbsKQzZU6z6kXH+el8qzTzyaGFGKc8EArwA1YlKkuME/rMK5BldCJUeiCUsZogXnlR2+WuyMK2XikqxGktYzSySvODLgngi6Ysz31gatTpOAeCo6DNyVj5AXyrN7qfs4McGKsI1bRrS7Gkwqu+xhKXVhdX9Eizp3HMdWLAxODEKy7WBq4ZG8IXAyti+PV+jVkc5twMAviZD54VcHp7roJDLs2NrZ4+lMa0VOQw+D8QSzTnQ7Bu1Ovb+/B3xRFIQI2SUIFdIqg3kARpbDgMjQBlA8SfXkzGq97JRK2Mz0rxXvFyTQLZYQTyRRHxWLY/a9oJGrS4Y5I3SjPjaQKRjEAl5roNPbz9gcGIUyIk2xDrAuZEWndxqZeNUlHqGU6wAqaoloK4ZAoHU3AJbgOdulc1eJsHPXB5UPic9ee5WcXFc9hUkgYAc/yDI2RJ0nfqJlsB1cHFcZhVQNQFqSoCYigIBmXm7NiobJzVcLxUBqNNSJVSVHmjU6gJjHaJuzXly3UCrBvQMp/Dp7Qecu1Whf9NC4jclYYjwG5v47IywEJkHeGltMnvqB3znk5uPDnzhYgRMHyq1gwlxOU0ZAT7/da6WQfqAYN+7z67rQB6jTOjlBWgUhHlkQxicGGVNiXmAXFPdL7VNv+p+yXjfJ4N6AtAq6b2AmBZB7vdcB9gHrEzzXO6EgFkY6Xe9PJt2OE7a2Hx/dfL+I/BmD2tjn0EToDFIBW2+vwUkbZAbvcX1ruz2xuX0XBWYGGXjKjWhrNsI8/Pzwvnm7q5AqIVbmJ+fxy59z6GQy2MmYeH2l1+rCVgZv27jkUy2wrW0tKS9z3H0IWh2xlGRwMVxGT3DKfSl0s1nAIo9HW98crL5DPDOnTvs2rt377RGgVYY4okkWwO+XjBkT/kaiQyVcdW5lgjXmBgBKih///qLcSGKAuoL9ByhGxBLqOc6xm1YxsuXL9nndmvAW1zvovG7yWijVg98/k+DHh4eAhBjTp8PDw/x6tUr7O7uYtgJ7g29cn7qZj/slEBZRek4OTkpGHYch6Xe2thnZA0iybKgkMtj7upZgA5rY58RTyTxsyK/5XtM11Ev0EqPWUOF88dSkyilUjm3w9ZO/IsrwbEP6AoaDyENXz/9zYgAbxxoFq+g1A0kQOlxHSxPjWNrZ89ImPgIEAld+ukIdt8eQixhRfZC6H0BLS5eEwxedTcrYzMv3AgB+eUF0FJEsYTFbb+DdUUkAoxIm8HDvmENHwLOCxSG06MTDE6MslDQE3WTch55b8h7gRemfBhMEC0EGi+QJ2gxduTteVsiXEbQmxIgnBciEaC9RLZY8T3C4b1wYwRUiLpXuBYB8oKuLpisg8gEVJsZuTqarIPQBHR7yTANiMe1/4FBJmFlAI97uPmfEOBf65AnbrQXqGJLxm+0F7R7lsAbtzK28m2qCh1bA/zMVVt2Ha5VB7LFiu+9Im/cpCN2rBJGxf+TgEmKmfaGjggSoFUJz90qzt2qcWWMJErp4MEXoTD1QPnyOgzi2xuXjxZmBBL012Sr35FFSOHgPWCKG82CG2nHJmj3svrGCfCGTdTxtQkEvcwIQkc94LmOsACDHnwDHUhDQtR/9/4Xd7FjcmoWqDQAAAAASUVORK5CYII=',
offset: {
x: 2,
y: 2
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(36)
.setFrameHeight(102)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const walkLeftUpAction = new SpriteAction()
await walkLeftUpAction
.setAction('walk_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 3,
y: 2
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 2
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABZCAYAAADVeL+8AAAFcElEQVR4nLVZzUtbWRT/JUqURAkkIWlBeIUmuDHQrqQI3Ygu/Qtk6LLMUldDqYtAF10ozKrMLjDC7LNsEMSdm7GQboIRfBDQ99BHxQ/aQM0sknM9971z33upmQOB5H78zu/ee+75uEks//EXoiTXqPWldm9tKxE2bzIO6MbqIqzqvNZ3tHeA+rDfpMQInmvU+huri6gsr2Lm+ROt7+bkHC+XXyNXLMFzHdQbtb6kQATPNWr9Pzd/w3RlAelCBhP5itY/A+AGgFUFLAxWJClImhinipYIrCZmZ5EqWoM5xZI8RmqsLK8imZ0VJ0hiVefx5sWzwMFr4MQ6TH5eHuPu4nYwOYJAYM+t6ryadHdxizSOtX4CjiOhphgFdn91DQCwW22xX9xzmhRXPNfBq7e/R4ObWJgIHO0diMAiuH9yWN/hP38r4O/HXwPjAnvuuU4sBYBu39KKReY91w4FpTHkb74ff9VIkWjMvbWtRL1R6+eKJVhVfWCqaAWU2q02LANrI3PPdSIPlvrtVltkLYJ7a1uJ+pfTgAJ+G6ndcx0jsJn5UAEHkoC51L+cBvy60RS9ta3EzudDIysOKgGHgo8ipkgUG3xUlwAACX+A5j6Z3K8/fpIc7R0AkPc7AJ5r1PpvXjxTN8+qzgfsm6IPtZEpkgFwJZMSMDFNFa1BOMNAAf1OFzK4Gc6jOCoFa+2GSsDpQgZ3AOAOxqQLGQADu7+/ulYrkYJ1krPmwCQT+YoC5G1cZp4/Qe7VoiJIoqyFGqVrz8Ek4Il8BRP5CqYrC1qwVuDcIvgB/rzUY6i/7ebkXP1OFzLaqjU7t1tt7Hw+DLDnYH5lPdfGzcm5WlUyO6t2QYFzjaMIEZFWOJbr7xfa4kjwuHnK3cVtgH0s5mEKRvbnJCZnZVLmb49kblLA200BfeQDlVhHBuieaxv3z88+rm8fmymSQu6HjOCmfZRYe66DnmurSoQu5KOYR2VmY9kWikIjgcfJGcPEmM5xBaSEK+u5dmTKZ2RumsiVRUkA/LI0j+2zqchkNCwBJRELrpWVFWw3m8DZN3xAUAGBbp9NAQDyQ2UUR2llxmpufX0djuPgXbMp9E4pEgDw7+4DAe52RfBSqYRyuYxyuYylpSUAQKfTEUk4jnlrRPByuSwO9rd3Oh2USnLdD7AD9R/e3Nyc+pgUmkgQlmiKBOj/7VcYJQo8yqyiCJBwf58EHsoUk48g6Xa7xj7Pdcxhjqe++/v7AVCylm63qz4mCyIJWMvm0x/Y3t0F8HBgBCKB+U2RF7xifv6hCLwbKiChC0OmR6DNZhN5BmxkzvPz3apeNYgXdbjSOiNvt9qqjNHA+dvW/dX1oERvAcApNp/+UAQAPSvmNSu3Os3OqZKg78CDiXJL4kUCT2BplWQcGjgFWH8lAehlOwEns7NIZmfx6eN7VezyV7rINy7OxP+qMZ1dUKv89PE9i06nD9tClsJLElPiQ2W73Wor6+BjxbKFREriRSXD7SFgKiUBBCsLXoXRNTaFOdp/u9VWMZUXw6RYYx6XtZ89MfcTCmwLdz6jekoSMgINnIDjZrH+1+fQS3R/da0dUFwxVYKhT3+mpw5ezoeVmGIF/RgxXn8uUXmgsuWQN/SxpNB+JxcA/5Xy3D8nVyxpjivgcklGtXGJ3CSdPAGTZzNZCpeeayMF/c1LA6cv6UJmpP8jPNcZRqm23uYH99+0qC0hv05+W+pX4L9i41FbBvxP7y2h4Ka/w8YCPi4RwcflZ4zMpT/yHgWeLmQwXVlAqmjh5fJrbKwuPkqBAh/lAsUVdUPvr64HD8LjBrdbbVhVaH4iToUcCe6tbSV2GrX+BgDyE6bH9lFFvfz7D+4xoCT/AaNCPNJBtKgMAAAAAElFTkSuQmCC',
offset: {
x: 5,
y: 6
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAABZCAYAAABfVyb9AAAGbklEQVR4nL2az2sbRxTHv7KDYuwYg2x2UzBsIFp0sSA+mWDwRVhH/QWm5Fh6dE6loIMgRwd66KE3QUxz6Un0ZGMKhhxyqQ3KRZUDXjBNdrGXCtkiEVjqYf3Gs7szuzMrte+i/aV5n3kz8+bNm8lVfvgFOlJoNcai536tntMq6E4e6CrerW7AKpdC706OjtG8e68LogRQaDXGu9UN2JUqHj19HHp3/fEz1itbKBgmfM9Fs9UY60CkAhRajfFPL7/FnL2G+ZUFzC7bofePAFwDsMqAhcAyOhAzacp3qxvIG5ZQOStkaRF5wwr+Y5gqetUAAMCuVDGztKhcoFUu4cWzJ9LOqgxAtU+S26suBpc3QUEakLwk9gGrXGIFDy5vMI9u6D0pn0SUh2GawlGvDwBw2h0tgNQ+QAWriu+5eP7d99MBUK0NQZ4cHWspTwWIKkh69/7tG6b8S/eDMkBiH/A9VwkCCI9/nX6QaoGh56QWMvQcNj986X4IgaeJ1AJ+rZ5rthrjgmHCKoff5Q0rBua0O7DwH4wC33NTC6X3TrujVftUAL9WzzVPz2MQvNej577naitPBeAheGUi5bw0T8+V4wKlYejX6rnXB+9Tv2uenmspVwaIStKQ1I2IcrKYMDqd0swYDcdITo6OAQRW0AERAhRajfGLZ0+C6zsHY5VLseFHQQg940eBKkiiJ4wqn1laRB4BBN3Pryzg+u57PixTjRFjAFT7wAGVWE1J2QAAvODb+ZUFAMGwHPX6zCIAYFfurZUEIe2EVGtSAACzyzZTShILUp8+ZpFz3rBSY0QhQMEwQ7WRKRQFqbPLNmaXbQZhV6qJMaLUAry3o2F3e9WNfSd6RiAEkWQFKcCo18fQc2Lej1cYVT70HCmQNgCgP7MBwUppIgByu0PPyTS5iCSpHC1XnBaGnxwdCwOYpPkhEeD3v/5WhlCJnLQBeJFNQDIg1c4oBRB1QBnEqNcPfU/KB5c3qZbJNB2T8LWPdrTB5Q1GvT6bJacGELUCf08elJQPPSc1QJECZBmC0VhBxY8oWUDWjqI+QS5c1Y8IAfhAVFVoyEZny0wAusJbSJbGmRggq6OZGIBvx6HnMJDo7+VgGPuvSoguHwW1eo4KlfVmPhjlJeqYMgEAwJVZwt6nh4nrwyxTNi+pOaLt7W3sHR4Cn/7BK8SVyYaaqh9JBTBNEzs7O3BdFz8eHgq+eAgAWFZSlwEAAIrFIorFIjY3NwEA7969g2nex3mu6+LP/WxNkQpQLBaxurrK7i8uLmCaJorFovB73ZgwFYBXLronSF50sqYTecLV1dUQkNPuBBnVlQXlpHVmAF4xfz2/sqCVwk0FuLi4iN2fnZ2FnvPXZAFVSe0DZ2dn0ntSzD/TtUAiwLLbwf7+PnZ2dhLBXDdwOr7namfQpQD3SYqv2NvfD73b3t7WUpIJgIEYJl4ZwTW51z2BR+Q9oU62VAggSlIAANoBxMtvvqJ5eg5K4wBA071XriPyLRvDhF2pYs5ew5y9hrxhMRg+XCsYJtYrW6H/Ou2OcrouBkC1p+0aPg0jEqtcQuH5/d6Sbro2MUNCW3VJ4zoaD/iey5pHZedMKUMiEj6PfP3xM9789jPLlO5WNyZzxVa5FNuopDUA37YE8f7tGwbx6x8HsCtVAFCyQgiAT1CS8HuDovCLILpHBxhc3uD2qouZpUXYlaqSFYQWyBuWML5P6ly+52LU67O5YGZpUWkXNT4KDJNlt6j2o14f3aMD6dDircAv1zLnCcmUSctvkfiey5qCRJbcTgQgU9K1ivDryVGvz5oBSO6MiXlCUq6zLCMrkKQ1QwwgajKWcFZwrbwVVJtBKSTLsvqhZiCRNUMMgE9SU+11syX0vUpwwgBETgjQm9kA+ZCU9QNpE0wzH8BP5YkAIsqs+WLfc2OVEPWDEECa01BWrpFjmgHu23/OXotty+puRCaJyMIhC+hmuKYhDGC9shWbAae1X0Aimh2nkqZTkbxhIW9YWK9shSAe0FkxPrAEpt/+PAhF0c1WY/y/WSAq1CEZQLT9pzEknXYn5gui94kW0DmUJhJK71GCU7TJnbp5/eKZ3vlApvzuEAxwDkgOP/i1eo4B3F512SKE36CeRFSgGcD1x894hOmckNORB8DdGaAy2DkA2rKdtiMSAvi1eu51qzHeBQAuFcufhJi2L+CFHeGY9nlhVfkXzlPYj4l0VG0AAAAASUVORK5CYII=',
offset: {
x: 2,
y: 6
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(34)
.setFrameHeight(101)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const attackRightDownAction = new SpriteAction()
await attackRightDownAction
.setAction('attack_right_down')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 20,
y: 0
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 19,
y: 8
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 17,
y: 3
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(69)
.setFrameHeight(111)
.setFrameRate(5)
.setSprite(characterSprite)
.save()
const attackLeftUpAction = new SpriteAction()
await attackLeftUpAction
.setAction('attack_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 2,
y: 0
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 5,
y: 0
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 6,
y: 1
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(34)
.setFrameHeight(100)
.setFrameRate(5)
.setSprite(characterSprite)
.save()
const characterType = new CharacterType()
await characterType
.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0')
.setName('Male character')
.setName('New character type')
.setGender(CharacterGender.MALE)
.setRace(CharacterRace.HUMAN)
.setIsSelectable(true)
@ -489,7 +285,7 @@ export default class InitCommand extends BaseCommand {
private async createCharacterHair(): Promise<void> {
const hairSprite = new Sprite()
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1').setWidth(30).setHeight(40)
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1')
await hairSprite.save()
const frontAction = new SpriteAction()
@ -533,7 +329,7 @@ export default class InitCommand extends BaseCommand {
.save()
const characterHair = new CharacterHair()
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setColor('#1B1212').setIsSelectable(true).setSprite(hairSprite).save()
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setIsSelectable(true).setSprite(hairSprite).save()
}
private async createMap(): Promise<void> {
@ -552,8 +348,6 @@ export default class InitCommand extends BaseCommand {
private async createUser(): Promise<void> {
const user = new User()
await user.setId('6f9a58b4-172d-425e-b9ea-71e1d13d81ee').setUsername('root').setEmail('local@host').setPassword('password').setOnline(false).save()
const map = await this.mapRepository.getFirst()
if (!map) return
const character = new Character()
await character
@ -561,7 +355,7 @@ export default class InitCommand extends BaseCommand {
.setUser(user)
.setName('root')
.setRole('gm')
.setMap(map)
.setMap((await this.mapRepository.getFirst())!)
.setCharacterType(await this.characterTypeRepository.getFirst())
.setCharacterHair(await this.characterHairRepository.getFirst())
.save()

View File

@ -1,5 +1,5 @@
import { BaseCommand } from '@/application/base/baseCommand'
import MapManager from '@/managers/mapManager'
import { BaseCommand } from '#application/base/baseCommand'
import MapManager from '#managers/mapManager'
type CommandInput = string[]

View File

@ -1,8 +1,10 @@
import fs from 'fs'
import { BaseCommand } from '@/application/base/baseCommand'
import Storage from '@/application/storage'
import sharp from 'sharp'
import { BaseCommand } from '#application/base/baseCommand'
import Storage from '#application/storage'
export default class TilesCommand extends BaseCommand {
public async execute(): Promise<void> {
// Get all tiles

View File

@ -1,10 +1,12 @@
import { BaseController } from '@/application/base/baseController'
import config from '@/application/config'
import { loginAccountSchema, newPasswordSchema, registerAccountSchema, resetPasswordSchema } from '@/application/zodTypes'
import UserService from '@/services/userService'
import type { Request, Response } from 'express'
import jwt from 'jsonwebtoken'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import config from '#application/config'
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from '#application/zodTypes'
import UserService from '#services/userService'
export class AuthController extends BaseController {
/**
* Login user

View File

@ -1,13 +1,16 @@
import fs from 'fs'
import { BaseController } from '@/application/base/baseController'
import Storage from '@/application/storage'
import type { UUID } from '@/application/types'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import CharacterRepository from '@/repositories/characterRepository'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import type { Request, Response } from 'express'
import sharp from 'sharp'
import type { UUID } from '#application/types'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import Storage from '#application/storage'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface AvatarOptions {
characterTypeId: UUID
characterHairId?: UUID
@ -65,12 +68,9 @@ export class AvatarController extends BaseController {
return this.sendError(res, 'Body sprite file not found', 404)
}
// Get body sprite metadata
const bodyMetadata = await sharp(bodySpritePath).metadata()
let avatar = sharp(bodySpritePath).extend({
top: 0,
bottom: 0,
top: 2,
bottom: 2,
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
@ -79,21 +79,7 @@ export class AvatarController extends BaseController {
if (characterHair?.sprite?.id) {
const hairSpritePath = Storage.getPublicPath('sprites', characterHair.sprite.id, 'front.png')
if (fs.existsSync(hairSpritePath)) {
// Resize hair sprite to match body dimensions
const resizedHair = await sharp(hairSpritePath)
.resize(bodyMetadata.width, bodyMetadata.height, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
.toBuffer()
avatar = avatar.composite([
{
input: resizedHair,
left: 0,
top: -27 // Apply vertical offset
}
])
avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }])
}
}
}
@ -101,7 +87,6 @@ export class AvatarController extends BaseController {
res.setHeader('Content-Type', 'image/png')
return avatar.pipe(res)
} catch (error) {
console.error('Avatar generation error:', error)
return this.sendError(res, 'Error generating avatar', 500)
}
}

View File

@ -1,12 +1,13 @@
import { BaseController } from '@/application/base/baseController'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import MapObjectRepository from '@/repositories/mapObjectRepository'
import MapRepository from '@/repositories/mapRepository'
import SpriteRepository from '@/repositories/spriteRepository'
import TileRepository from '@/repositories/tileRepository'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapObjectRepository from '#repositories/mapObjectRepository'
import MapRepository from '#repositories/mapRepository'
import SpriteRepository from '#repositories/spriteRepository'
import TileRepository from '#repositories/tileRepository'
export class CacheController extends BaseController {
/**
* Serve a list of tiles and send as JSON

View File

@ -1,7 +1,8 @@
import { BaseController } from '@/application/base/baseController'
import Storage from '@/application/storage'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import Storage from '#application/storage'
export class TexturesController extends BaseController {
/**
* Download texture

View File

@ -1,15 +1,18 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { CharacterEquipment } from '@/entities/characterEquipment'
import type { CharacterHair } from '@/entities/characterHair'
import type { CharacterItem } from '@/entities/characterItem'
import type { CharacterType } from '@/entities/characterType'
import type { Chat } from '@/entities/chat'
import type { Map } from '@/entities/map'
import type { User } from '@/entities/user'
import { Collection, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { CharacterEquipment } from '#entities/characterEquipment'
import type { CharacterHair } from '#entities/characterHair'
import type { CharacterItem } from '#entities/characterItem'
import type { CharacterType } from '#entities/characterType'
import type { Chat } from '#entities/chat'
import type { Map } from '#entities/map'
import type { User } from '#entities/user'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseCharacter extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import { CharacterEquipmentSlotType } from '@/application/enums'
import type { UUID } from '@/application/types'
import type { Character } from '@/entities/character'
import type { CharacterItem } from '@/entities/characterItem'
import { Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Character } from '#entities/character'
import type { CharacterItem } from '#entities/characterItem'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterEquipmentSlotType } from '#application/enums'
export class BaseCharacterEquipment extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import { CharacterGender } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Character } from '@/entities/character'
import { Sprite } from '@/entities/sprite'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender } from '#application/enums'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'
export class BaseCharacterHair extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ -16,14 +19,11 @@ export class BaseCharacterHair extends BaseEntity {
@Property()
gender: CharacterGender = CharacterGender.MALE
@Property()
color: string = '#000000'
@Property()
isSelectable = false
@ManyToOne()
sprite!: Sprite
sprite?: Sprite
@Property()
createdAt = new Date()
@ -58,15 +58,6 @@ export class BaseCharacterHair extends BaseEntity {
return this.gender
}
setColor(color: string) {
this.color = color
return this
}
getColor() {
return this.color
}
setIsSelectable(isSelectable: boolean) {
this.isSelectable = isSelectable
return this

View File

@ -1,10 +1,13 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Character } from '@/entities/character'
import type { Item } from '@/entities/item'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Character } from '#entities/character'
import type { Item } from '#entities/item'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseCharacterItem extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import { CharacterGender, CharacterRace } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Character } from '@/entities/character'
import { Sprite } from '@/entities/sprite'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender, CharacterRace } from '#application/enums'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'
export class BaseCharacterType extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,10 +1,13 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Character } from '@/entities/character'
import type { Map } from '@/entities/map'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Character } from '#entities/character'
import type { Map } from '#entities/map'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseChat extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import { ItemRarity, ItemType } from '@/application/enums'
import type { UUID } from '@/application/types'
import { CharacterItem } from '@/entities/characterItem'
import { Sprite } from '@/entities/sprite'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { ItemType, ItemRarity } from '#application/enums'
import { CharacterItem } from '#entities/characterItem'
import { Sprite } from '#entities/sprite'
export class BaseItem extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ -25,8 +28,8 @@ export class BaseItem extends BaseEntity {
@Enum(() => ItemRarity)
rarity: ItemRarity = ItemRarity.COMMON
@ManyToOne()
sprite!: Sprite
@ManyToOne(() => Sprite)
sprite?: Sprite
@Property()
createdAt = new Date()

View File

@ -1,17 +1,20 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { MapEffect } from '@/entities/mapEffect'
import type { MapEventTile } from '@/entities/mapEventTile'
import type { PlacedMapObject } from '@/entities/placedMapObject'
import { Collection, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { MapEffect } from '#entities/mapEffect'
import type { MapEventTile } from '#entities/mapEventTile'
import type { PlacedMapObject } from '#entities/placedMapObject'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseMap extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name: string = ''
name!: string
@Property()
width = 10
@ -19,7 +22,7 @@ export class BaseMap extends BaseEntity {
@Property()
height = 10
@Property({ type: 'json' })
@Property({ type: 'json', nullable: true })
tiles: Array<Array<string>> = []
@Property()

View File

@ -1,9 +1,12 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Map } from '@/entities/map'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Map } from '#entities/map'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseMapEffect extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import { MapEventTileType } from '@/application/enums'
import type { UUID } from '@/application/types'
import type { Map } from '@/entities/map'
import type { MapEventTileTeleport } from '@/entities/mapEventTileTeleport'
import { Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Map } from '#entities/map'
import type { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
import { BaseEntity } from '#application/base/baseEntity'
import { MapEventTileType } from '#application/enums'
export class BaseMapEventTile extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ -17,12 +20,12 @@ export class BaseMapEventTile extends BaseEntity {
type!: MapEventTileType
@Property()
positionX: number = 0
positionX!: number
@Property()
positionY: number = 0
positionY!: number
@OneToOne({ eager: true, deleteRule: 'cascade', orphanRemoval: true })
@OneToOne({ eager: true })
teleport?: MapEventTileTeleport
setId(id: UUID) {

View File

@ -1,15 +1,18 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Map } from '@/entities/map'
import type { MapEventTile } from '@/entities/mapEventTile'
import { ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Map } from '#entities/map'
import type { MapEventTile } from '#entities/mapEventTile'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseMapEventTileTeleport extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@OneToOne({ deleteRule: 'cascade', orphanRemoval: true })
@OneToOne({ deleteRule: 'cascade' })
mapEventTile!: MapEventTile
@ManyToOne({ deleteRule: 'cascade', eager: true })

View File

@ -1,8 +1,11 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseMapObject extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ -10,11 +13,8 @@ export class BaseMapObject extends BaseEntity {
@Property()
name!: string
@Property({ type: 'json' })
tags: string[] = []
@Property({ type: 'json' })
depthOffsets: number[] = [0]
@Property({ type: 'json', nullable: true })
tags?: any
@Property({ type: 'decimal', precision: 10, scale: 2 })
originX = 0
@ -64,14 +64,6 @@ export class BaseMapObject extends BaseEntity {
return this.tags
}
setDepthOffsets(offsets: number[]) {
this.depthOffsets = offsets
}
getDepthOffsets() {
return this.depthOffsets
}
setOriginX(originX: number) {
this.originX = originX
return this

View File

@ -1,9 +1,12 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { User } from '@/entities/user'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { User } from '#entities/user'
import { BaseEntity } from '#application/base/baseEntity'
export class BasePasswordResetToken extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,10 +1,13 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Map } from '@/entities/map'
import type { MapObject } from '@/entities/mapObject'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Map } from '#entities/map'
import type { MapObject } from '#entities/mapObject'
import { BaseEntity } from '#application/base/baseEntity'
export class BasePlacedMapObject extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,9 +1,12 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import { SpriteAction } from '@/entities/spriteAction'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { SpriteAction } from '#entities/spriteAction'
export class BaseSprite extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ -11,15 +14,9 @@ export class BaseSprite extends BaseEntity {
@Property()
name!: string
@OneToMany({ mappedBy: 'sprite', orphanRemoval: true })
@OneToMany(() => SpriteAction, (action) => action.sprite)
spriteActions = new Collection<SpriteAction>(this)
@Property({ nullable: true })
width: number | null = null
@Property({ nullable: true })
height: number | null = null
@Property()
createdAt = new Date()
@ -53,24 +50,6 @@ export class BaseSprite extends BaseEntity {
return this.spriteActions
}
setWidth(width: number | null) {
this.width = width
return this
}
getWidth() {
return this.width
}
setHeight(height: number | null) {
this.height = height
return this
}
getHeight() {
return this.height
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this

View File

@ -1,9 +1,12 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import type { Sprite } from '@/entities/sprite'
import { ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import type { Sprite } from '#entities/sprite'
import { BaseEntity } from '#application/base/baseEntity'
export interface SpriteImage {
url: string
offset: {

View File

@ -1,8 +1,11 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseTile extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto'
import { BaseEntity } from '@/application/base/baseEntity'
import type { UUID } from '@/application/types'
import { Character } from '@/entities/character'
import { PasswordResetToken } from '@/entities/passwordResetToken'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import bcrypt from 'bcryptjs'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { Character } from '#entities/character'
import { PasswordResetToken } from '#entities/passwordResetToken'
export class BaseUser extends BaseEntity {
@PrimaryKey()
id = randomUUID()

View File

@ -1,6 +1,7 @@
import { BaseEntity } from '@/application/base/baseEntity'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseWorld extends BaseEntity {
@PrimaryKey()
date = new Date()

View File

@ -1,5 +1,6 @@
import { BaseCharacter } from '@/entities/base/character'
import { Entity } from '@mikro-orm/core'
import { BaseCharacter } from '#entities/base/character'
@Entity()
export class Character extends BaseCharacter {}

View File

@ -1,5 +1,6 @@
import { BaseCharacterEquipment } from '@/entities/base/characterEquipment'
import { Entity } from '@mikro-orm/core'
import { BaseCharacterEquipment } from '#entities/base/characterEquipment'
@Entity()
export class CharacterEquipment extends BaseCharacterEquipment {}

View File

@ -1,6 +1,7 @@
import { BaseCharacterHair } from '@/entities/base/characterHair'
import { Entity } from '@mikro-orm/core'
import { BaseCharacterHair } from '#entities/base/characterHair'
@Entity()
export class CharacterHair extends BaseCharacterHair {
public async cache() {

View File

@ -1,5 +1,6 @@
import { BaseCharacterItem } from '@/entities/base/characterItem'
import { Entity } from '@mikro-orm/core'
import { BaseCharacterItem } from '#entities/base/characterItem'
@Entity()
export class CharacterItem extends BaseCharacterItem {}

View File

@ -1,6 +1,7 @@
import { BaseCharacterType } from '@/entities/base/characterType'
import { Entity } from '@mikro-orm/core'
import { BaseCharacterType } from '#entities/base/characterType'
@Entity()
export class CharacterType extends BaseCharacterType {
public async cache() {

View File

@ -1,5 +1,6 @@
import { BaseChat } from '@/entities/base/chat'
import { Entity } from '@mikro-orm/core'
import { BaseChat } from '#entities/base/chat'
@Entity()
export class Chat extends BaseChat {}

View File

@ -1,5 +1,6 @@
import { BaseItem } from '@/entities/base/item'
import { Entity } from '@mikro-orm/core'
import { BaseItem } from '#entities/base/item'
@Entity()
export class Item extends BaseItem {}

View File

@ -1,8 +1,8 @@
import { BaseMap } from '@/entities/base/map'
import { Entity } from '@mikro-orm/core'
import { BaseMap } from '#entities/base/map'
export type MapCacheT = ReturnType<Map['cache']> | {}
export type MapEditorMapT = ReturnType<Map['mapEditorObject']> | {}
@Entity()
export class Map extends BaseMap {
@ -36,42 +36,4 @@ export class Map extends BaseMap {
return {}
}
}
public async mapEditorObject() {
try {
await this.getPlacedMapObjects().load()
await this.getMapEffects().load()
await this.getMapEventTiles().load()
return {
id: this.getId(),
name: this.getName(),
width: this.getWidth(),
height: this.getHeight(),
tiles: this.getTiles(),
pvp: this.getPvp(),
createdAt: this.getCreatedAt(),
updatedAt: this.getUpdatedAt(),
placedMapObjects: this.getPlacedMapObjects(),
mapEffects: this.getMapEffects(),
mapEventTiles: this.getMapEventTiles().map((mapEventTile) => ({
id: mapEventTile.getId(),
type: mapEventTile.getType(),
positionX: mapEventTile.getPositionX(),
positionY: mapEventTile.getPositionY(),
teleport: mapEventTile.getTeleport()
? {
toMap: mapEventTile.getTeleport()?.getToMap().getId(),
toPositionX: mapEventTile.getTeleport()?.getToPositionX(),
toPositionY: mapEventTile.getTeleport()?.getToPositionY(),
toRotation: mapEventTile.getTeleport()?.getToRotation()
}
: undefined
}))
}
} catch (error) {
console.error(error)
return {}
}
}
}

View File

@ -1,5 +1,6 @@
import { BaseMapEffect } from '@/entities/base/mapEffect'
import { Entity } from '@mikro-orm/core'
import { BaseMapEffect } from '#entities/base/mapEffect'
@Entity()
export class MapEffect extends BaseMapEffect {}

View File

@ -1,5 +1,6 @@
import { BaseMapEventTile } from '@/entities/base/mapEventTile'
import { Entity } from '@mikro-orm/core'
import { BaseMapEventTile } from '#entities/base/mapEventTile'
@Entity()
export class MapEventTile extends BaseMapEventTile {}

View File

@ -1,5 +1,6 @@
import { BaseMapEventTileTeleport } from '@/entities/base/mapEventTileTeleport'
import { Entity } from '@mikro-orm/core'
import { BaseMapEventTileTeleport } from '#entities/base/mapEventTileTeleport'
@Entity()
export class MapEventTileTeleport extends BaseMapEventTileTeleport {}

View File

@ -1,6 +1,7 @@
import { BaseMapObject } from '@/entities/base/mapObject'
import { Entity } from '@mikro-orm/core'
import { BaseMapObject } from '#entities/base/mapObject'
@Entity()
export class MapObject extends BaseMapObject {
public async cache() {

View File

@ -1,5 +1,6 @@
import { BasePasswordResetToken } from '@/entities/base/passwordResetToken'
import { Entity } from '@mikro-orm/core'
import { BasePasswordResetToken } from '#entities/base/passwordResetToken'
@Entity()
export class PasswordResetToken extends BasePasswordResetToken {}

View File

@ -1,5 +1,6 @@
import { BasePlacedMapObject } from '@/entities/base/placedMapObject'
import { Entity } from '@mikro-orm/core'
import { BasePlacedMapObject } from '#entities/base/placedMapObject'
@Entity()
export class PlacedMapObject extends BasePlacedMapObject {}

View File

@ -1,6 +1,7 @@
import { BaseSprite } from '@/entities/base/sprite'
import { Entity } from '@mikro-orm/core'
import { BaseSprite } from '#entities/base/sprite'
@Entity()
export class Sprite extends BaseSprite {
public async cache() {

View File

@ -1,5 +1,6 @@
import { BaseSpriteAction } from '@/entities/base/spriteAction'
import { Entity } from '@mikro-orm/core'
import { BaseSpriteAction } from '#entities/base/spriteAction'
@Entity()
export class SpriteAction extends BaseSpriteAction {}

View File

@ -1,6 +1,7 @@
import { BaseTile } from '@/entities/base/tile'
import { Entity } from '@mikro-orm/core'
import { BaseTile } from '#entities/base/tile'
@Entity()
export class Tile extends BaseTile {
public async cache() {

View File

@ -1,5 +1,6 @@
import { BaseUser } from '@/entities/base/user'
import { Entity } from '@mikro-orm/core'
import { BaseUser } from '#entities/base/user'
@Entity()
export class User extends BaseUser {}

View File

@ -1,5 +1,6 @@
import { BaseWorld } from '@/entities/base/world'
import { Entity } from '@mikro-orm/core'
import { BaseWorld } from '#entities/base/world'
@Entity()
export class World extends BaseWorld {}

View File

@ -1,16 +1,14 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { ZCharacterConnect } from '@/application/zodTypes'
import MapManager from '@/managers/mapManager'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import CharacterRepository from '@/repositories/characterRepository'
import TeleportService from '@/services/characterTeleportService'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository'
import TeleportService from '#services/characterTeleportService'
interface CharacterConnectPayload {
characterId: UUID
characterHairId: UUID | null
newNickname?: string
characterHairId?: UUID
}
export default class CharacterConnectEvent extends BaseEvent {
@ -18,40 +16,23 @@ export default class CharacterConnectEvent extends BaseEvent {
private readonly characterRepository = new CharacterRepository()
public listen(): void {
this.socket.on(SocketEvent.CHARACTER_CONNECT, this.handleEvent.bind(this))
this.socket.on('character:connect', this.handleEvent.bind(this))
}
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
try {
if (data.newNickname === '') data.newNickname = undefined
const result = ZCharacterConnect.safeParse(data)
if (!result.success) {
this.sendNotificationAndLog(result.error?.errors[0]?.message ?? 'Invalid data')
return
}
if (await this.checkForActiveCharacters()) {
this.sendNotificationAndLog('You are already connected to another character')
this.emitError('You are already connected to another character')
return
}
let character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId)
const character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId)
if (!character) {
this.sendNotificationAndLog('Character not found or does not belong to this user')
this.emitError('Character not found or does not belong to this user')
return
}
if (data.newNickname) {
const existingCharacter = await this.characterRepository.getByName(data.newNickname)
if (existingCharacter) {
this.sendNotificationAndLog('Nickname already in use: ' + data.newNickname)
return
}
await character.setName(data.newNickname).save()
}
// Set character id
this.socket.characterId = character.id
@ -59,8 +40,6 @@ export default class CharacterConnectEvent extends BaseEvent {
if (data.characterHairId !== undefined && data.characterHairId !== null) {
const characterHair = await this.characterHairRepository.getById(data.characterHairId)
await character.setCharacterHair(characterHair).save()
} else {
await character.setCharacterHair(null).save()
}
// Emit character connect event

View File

@ -1,84 +1,72 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { ZCharacterCreate } from '@/application/zodTypes'
import { Character } from '@/entities/character'
import CharacterRepository from '@/repositories/characterRepository'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import MapRepository from '@/repositories/mapRepository'
import UserRepository from '@/repositories/userRepository'
import { z, ZodError } from 'zod'
import { ZodError } from 'zod'
const MAX_CHARACTERS = 4
import { BaseEvent } from '#application/base/baseEvent'
import { ZCharacterCreate } from '#application/zodTypes'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapRepository from '#repositories/mapRepository'
import UserRepository from '#repositories/userRepository'
export default class CharacterCreateEvent extends BaseEvent {
private readonly userRepository: UserRepository = new UserRepository()
private readonly characterRepository: CharacterRepository = new CharacterRepository()
private readonly characterTypeRepository: CharacterTypeRepository = new CharacterTypeRepository()
private readonly mapRepository: MapRepository = new MapRepository()
public listen(): void {
this.socket.on(SocketEvent.CHARACTER_CREATE, this.handleEvent.bind(this))
this.socket.on('character:create', this.handleEvent.bind(this))
}
private async handleEvent(data: z.infer<typeof ZCharacterCreate>, callback: (success: boolean) => void): Promise<void> {
private async handleEvent(data: any): Promise<any> {
// zod validate
try {
const validatedData = ZCharacterCreate.parse(data)
await this.createCharacter(validatedData)
callback(true)
} catch (error: unknown) {
this.returnError(error)
callback(false)
data = ZCharacterCreate.parse(data)
const userRepository = new UserRepository()
const characterRepository = new CharacterRepository()
const characterTypeRepository = new CharacterTypeRepository()
const mapRepository = new MapRepository()
const user = await userRepository.getById(this.socket.userId!)
if (!user) {
return this.socket.emit('notification', { title: 'Error', message: 'You are not logged in' })
}
// Check if character name already exists
const characterExists = await characterRepository.getByName(data.name)
if (characterExists) {
return this.socket.emit('notification', { title: 'Error', message: 'Character name already exists' })
}
let characters: Character[] = await characterRepository.getByUserId(user.getId())
if (characters.length >= 4) {
return this.socket.emit('notification', { title: 'Error', message: 'You can only create 4 characters' })
}
// @TODO: Change to default location
const map = await mapRepository.getFirst()
// @TODO: Change to selected character type
const characterType = await characterTypeRepository.getFirst()
const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setMap(map!).setCharacterType(characterType).save()
if (!newCharacter) {
return this.socket.emit('notification', { title: 'Error', message: 'Failed to create character. Please try again (later).' })
}
characters = [...characters, newCharacter]
this.socket.emit('character:create:success')
this.socket.emit('character:list', characters)
this.logger.info('character:create success')
} catch (error: any) {
this.logger.error(`character:create error: ${error.message}`)
if (error instanceof ZodError) {
return this.socket.emit('notification', { title: 'Error', message: error.issues[0]!.message })
}
return this.socket.emit('notification', { title: 'Error', message: 'Could not create character. Please try again (later).' })
}
}
private async createCharacter(data: z.infer<typeof ZCharacterCreate>): Promise<void> {
const user = await this.userRepository.getById(this.socket.userId!)
if (!user) {
throw new Error('You are not logged in')
}
const characterExists = await this.characterRepository.getByName(data.name)
if (characterExists) {
throw new Error('Character name already exists')
}
let characters = await this.characterRepository.getByUserId(user.getId())
if (characters.length >= MAX_CHARACTERS) {
throw new Error(`You can only create ${MAX_CHARACTERS} characters`)
}
const map = await this.mapRepository.getFirst()
if (!map) {
throw new Error('No default map found')
}
const characterType = await this.characterTypeRepository.getFirst()
if (!characterType) {
throw new Error('No character type found')
}
const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setMap(map).setCharacterType(characterType).save()
characters = await this.characterRepository.getByUserId(user.getId())
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
this.logger.info('character:create success')
}
private returnError(error: unknown): void {
this.logger.error(`character:create error: ${error instanceof Error ? error.message : 'Unknown error'}`)
let errorMessage = 'Could not create character. Please try again later.'
if (error instanceof ZodError) {
errorMessage = error.issues[0]?.message || errorMessage
} else if (error instanceof Error) {
errorMessage = error.message
}
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Error',
message: errorMessage
})
}
}

View File

@ -1,8 +1,8 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Character } from '@/entities/character'
import CharacterRepository from '@/repositories/characterRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
type TypePayload = {
characterId: UUID
@ -14,18 +14,18 @@ type TypeResponse = {
export default class CharacterDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHARACTER_DELETE, this.handleEvent.bind(this))
this.socket.on('character:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
try {
const characterRepository = new CharacterRepository()
await (await characterRepository.getByUserAndId(this.socket.userId, data.characterId))?.delete()
const characters: Character[] = await characterRepository.getByUserId(this.socket.userId)
await (await characterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete()
const characters: Character[] = await characterRepository.getByUserId(this.socket.userId!)
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
this.socket.emit('character:list', characters)
} catch (error: any) {
return this.socket.emit(SocketEvent.NOTIFICATION, { message: 'Character delete failed. Please try again.' })
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
}
}
}

View File

@ -1,19 +1,18 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { Character } from '@/entities/character'
import CharacterRepository from '@/repositories/characterRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export default class CharacterListEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHARACTER_LIST, this.handleEvent.bind(this))
this.socket.on('character:list', this.handleEvent.bind(this))
}
private async handleEvent(data: any): Promise<void> {
try {
const characterRepository = new CharacterRepository()
let characters: Character[] = await characterRepository.getByUserId(this.socket.userId)
let characters: Character[] = await characterRepository.getByUserId(this.socket.userId!)
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
this.socket.emit('character:list', characters)
} catch (error: any) {
this.logger.error('character:list error', error.message)
}

View File

@ -1,7 +1,6 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import CharacterRepository from '@/repositories/characterRepository'
import ChatService from '@/services/chatService'
import { BaseEvent } from '#application/base/baseEvent'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -9,7 +8,7 @@ type TypePayload = {
export default class AlertCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
@ -26,7 +25,7 @@ export default class AlertCommandEvent extends BaseEvent {
return callback(false)
}
this.io.emit(SocketEvent.NOTIFICATION, { title: 'Message from GM', message: args.join(' ') })
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
return callback(true)
} catch (error: any) {
this.logger.error('chat:alert_command error', error.message)

View File

@ -1,8 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import DateManager from '@/managers/dateManager'
import CharacterRepository from '@/repositories/characterRepository'
import ChatService from '@/services/chatService'
import { BaseEvent } from '#application/base/baseEvent'
import DateManager from '#managers/dateManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -10,7 +9,7 @@ type TypePayload = {
export default class SetTimeCommand extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,10 +1,10 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import MapManager from '@/managers/mapManager'
import MapRepository from '@/repositories/mapRepository'
import TeleportService from '@/services/characterTeleportService'
import ChatService from '@/services/chatService'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository'
import TeleportService from '#services/characterTeleportService'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -12,7 +12,7 @@ type TypePayload = {
export default class TeleportCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void) {
@ -29,7 +29,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const args = ChatService.getArgs('teleport', data.message)
if (!args || args.length === 0 || args.length > 3) {
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: 'Usage: /teleport <mapId> [x] [y]'
})
@ -41,7 +41,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const targetY = args[2] ? parseInt(args[2], 10) : 0
if (!mapId || isNaN(targetX) || isNaN(targetY)) {
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: 'Invalid parameters. X and Y coordinates must be numbers.'
})
@ -51,7 +51,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const mapRepository = new MapRepository()
const map = await mapRepository.getById(mapId)
if (!map) {
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: 'Map not found'
})
@ -59,7 +59,7 @@ export default class TeleportCommandEvent extends BaseEvent {
}
if (character.map.id === map.id && targetX === character.positionX && targetY === character.positionY) {
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: 'You are already at that location'
})
@ -74,20 +74,20 @@ export default class TeleportCommandEvent extends BaseEvent {
})
if (!success) {
return this.socket.emit(SocketEvent.NOTIFICATION, {
return this.socket.emit('notification', {
title: 'Server message',
message: 'Failed to teleport'
})
}
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: `Teleported to ${map.name} (${targetX}, ${targetY})`
})
this.logger.info('teleport', `Character ${character.id} teleported to map ${map.id} at position (${targetX}, ${targetY})`)
} catch (error: any) {
this.logger.error(`Error in teleport command: ${error.message}`)
this.socket.emit(SocketEvent.NOTIFICATION, {
this.socket.emit('notification', {
title: 'Server message',
message: 'An error occurred while teleporting'
})

View File

@ -1,7 +1,6 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import WeatherManager from '@/managers/weatherManager'
import ChatService from '@/services/chatService'
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -9,7 +8,7 @@ type TypePayload = {
export default class ToggleFogCommand extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,7 +1,6 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import WeatherManager from '@/managers/weatherManager'
import ChatService from '@/services/chatService'
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -9,7 +8,7 @@ type TypePayload = {
export default class ToggleRainCommand extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import MapManager from '@/managers/mapManager'
import ChatService from '@/services/chatService'
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
@ -9,22 +9,31 @@ type TypePayload = {
export default class ChatMessageEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!data.message || ChatService.isCommand(data.message)) return
if (!data.message || ChatService.isCommand(data.message)) {
return callback(false)
}
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) {
this.logger.error('chat:message error', 'Character not found')
return
return callback(false)
}
const character = mapCharacter.character
if (await ChatService.sendMapMessage(character.getId(), data.message)) {
const mapRepository = new MapRepository()
const map = await mapRepository.getById(character.map.id)
if (!map) {
this.logger.error('chat:message error', 'Map not found')
return callback(false)
}
if (await ChatService.sendMapMessage(character.getId(), map.getId(), data.message)) {
return callback(true)
}

View File

@ -1,10 +1,9 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import MapManager from '@/managers/mapManager'
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
export default class DisconnectEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.DISCONNECT, this.handleEvent.bind(this))
this.socket.on('disconnect', this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {
@ -14,7 +13,7 @@ export default class DisconnectEvent extends BaseEvent {
return
}
this.io.emit(SocketEvent.USER_DISCONNECT, this.socket.userId)
this.io.emit('user:disconnect', this.socket.userId)
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) {

View File

@ -1,28 +1,17 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { CharacterGender, SocketEvent } from '@/application/enums'
import { CharacterHair } from '@/entities/characterHair'
import SpriteRepository from '@/repositories/spriteRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair'
export default class CharacterHairCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERHAIR_CREATE, this.handleEvent.bind(this))
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
// Get first sprite
const spriteRepository = new SpriteRepository()
const firstSprite = await spriteRepository.getFirst()
if (!firstSprite) {
this.sendNotificationAndLog('No sprites found')
return callback(false)
}
const newCharacterHair = new CharacterHair()
await newCharacterHair.setName('New hair').setGender(CharacterGender.MALE).setSprite(firstSprite).save()
await newCharacterHair.setName('New hair').save()
return callback(true)
} catch (error) {

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import CharacterHairRepository from '#repositories/characterHairRepository'
interface IPayload {
id: UUID
@ -9,7 +9,7 @@ interface IPayload {
export default class CharacterHairDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERHAIR_REMOVE, this.handleEvent.bind(this))
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,13 +1,12 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { CharacterHair } from '@/entities/characterHair'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair'
import CharacterHairRepository from '#repositories/characterHairRepository'
interface IPayload {}
export default class characterHairListEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERHAIR_LIST, this.handleEvent.bind(this))
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {

View File

@ -1,21 +1,21 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { CharacterGender, SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import SpriteRepository from '@/repositories/spriteRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender } from '#application/enums'
import CharacterHairRepository from '#repositories/characterHairRepository'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {
id: UUID
name: string
gender: CharacterGender
color: string
isSelectable: boolean
spriteId: UUID
}
export default class CharacterHairUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERHAIR_UPDATE, this.handleEvent.bind(this))
this.socket.on('gm:characterHair:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
@ -30,7 +30,7 @@ export default class CharacterHairUpdateEvent extends BaseEvent {
const characterHair = await characterHairRepository.getById(data.id)
if (!characterHair) return callback(false)
await characterHair.setName(data.name).setGender(data.gender).setColor(data.color).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save()
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save()
return callback(true)
} catch (error) {

View File

@ -1,10 +1,9 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { CharacterType } from '@/entities/characterType'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterType } from '#entities/characterType'
export default class CharacterTypeCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERTYPE_CREATE, this.handleEvent.bind(this))
this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface IPayload {
id: UUID
@ -9,7 +9,7 @@ interface IPayload {
export default class CharacterTypeDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERTYPE_REMOVE, this.handleEvent.bind(this))
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,13 +1,12 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { CharacterType } from '@/entities/characterType'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterType } from '#entities/characterType'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface IPayload {}
export default class CharacterTypeListEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERTYPE_LIST, this.handleEvent.bind(this))
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {

View File

@ -1,8 +1,9 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { CharacterGender, CharacterRace, SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import CharacterTypeRepository from '@/repositories/characterTypeRepository'
import SpriteRepository from '@/repositories/spriteRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender, CharacterRace } from '#application/enums'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {
id: UUID
@ -15,7 +16,7 @@ type Payload = {
export default class CharacterTypeUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_CHARACTERTYPE_UPDATE, this.handleEvent.bind(this))
this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,11 +1,11 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { ItemRarity, ItemType, SocketEvent } from '@/application/enums'
import { Item } from '@/entities/item'
import SpriteRepository from '@/repositories/spriteRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { ItemRarity, ItemType } from '#application/enums'
import { Item } from '#entities/item'
import SpriteRepository from '#repositories/spriteRepository'
export default class ItemCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_ITEM_CREATE, this.handleEvent.bind(this))
this.socket.on('gm:item:create', this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {
@ -14,10 +14,7 @@ export default class ItemCreateEvent extends BaseEvent {
const spriteRepository = new SpriteRepository()
const sprite = await spriteRepository.getFirst()
if (!sprite) {
this.sendNotificationAndLog('No sprites found')
return callback(false)
}
if (!sprite) return callback(false)
const newItem = new Item()
await newItem.setName('New Item').setItemType(ItemType.WEAPON).setStackable(false).setRarity(ItemRarity.COMMON).setSprite(sprite).save()

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import ItemRepository from '@/repositories/itemRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import ItemRepository from '#repositories/itemRepository'
interface IPayload {
id: UUID
@ -9,7 +9,7 @@ interface IPayload {
export default class ItemDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_ITEM_REMOVE, this.handleEvent.bind(this))
this.socket.on('gm:item:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,13 +1,12 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { Item } from '@/entities/item'
import ItemRepository from '@/repositories/itemRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { Item } from '#entities/item'
import ItemRepository from '#repositories/itemRepository'
interface IPayload {}
export default class ItemListEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_ITEM_LIST, this.handleEvent.bind(this))
this.socket.on('gm:item:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {

View File

@ -1,8 +1,9 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { ItemRarity, ItemType, SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import ItemRepository from '@/repositories/itemRepository'
import SpriteRepository from '@/repositories/spriteRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { ItemType, ItemRarity } from '#application/enums'
import ItemRepository from '#repositories/itemRepository'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {
id: UUID
@ -16,7 +17,7 @@ type Payload = {
export default class ItemUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_ITEM_UPDATE, this.handleEvent.bind(this))
this.socket.on('gm:item:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,13 +1,12 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { MapObject } from '@/entities/mapObject'
import MapObjectRepository from '@/repositories/mapObjectRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
import MapObjectRepository from '#repositories/mapObjectRepository'
interface IPayload {}
export default class MapObjectListEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_MAPOBJECT_LIST, this.handleEvent.bind(this))
this.socket.on('gm:mapObject:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: MapObject[]) => void): Promise<void> {

View File

@ -1,9 +1,10 @@
import fs from 'fs'
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import Storage from '@/application/storage'
import type { UUID } from '@/application/types'
import MapObjectRepository from '@/repositories/mapObjectRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import MapObjectRepository from '#repositories/mapObjectRepository'
interface IPayload {
mapObjectId: UUID
@ -11,7 +12,7 @@ interface IPayload {
export default class MapObjectRemoveEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_MAPOBJECT_REMOVE, this.handleEvent.bind(this))
this.socket.on('gm:mapObject:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,13 +1,12 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import MapObjectRepository from '@/repositories/mapObjectRepository'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import MapObjectRepository from '#repositories/mapObjectRepository'
type Payload = {
id: UUID
name: string
tags: string[]
depthOffsets: number[]
originX: number
originY: number
frameRate: number
@ -17,7 +16,7 @@ type Payload = {
export default class MapObjectUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_MAPOBJECT_UPDATE, this.handleEvent.bind(this))
this.socket.on('gm:mapObject:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
@ -31,7 +30,6 @@ export default class MapObjectUpdateEvent extends BaseEvent {
if (data.name !== undefined) mapObject.name = data.name
if (data.tags !== undefined) mapObject.tags = data.tags
if (data.depthOffsets !== undefined) mapObject.depthOffsets = data.depthOffsets
if (data.originX !== undefined) mapObject.originX = data.originX
if (data.originY !== undefined) mapObject.originY = data.originY
if (data.frameRate !== undefined) mapObject.frameRate = data.frameRate
@ -42,7 +40,7 @@ export default class MapObjectUpdateEvent extends BaseEvent {
return callback(true)
} catch (error) {
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Error', message: 'Failed to update mapObject.' })
this.socket.emit('notification', { title: 'Error', message: 'Failed to update mapObject.' })
return callback(false)
}
}

View File

@ -1,18 +1,19 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import Storage from '@/application/storage'
import { MapObject } from '@/entities/mapObject'
import sharp from 'sharp'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { MapObject } from '#entities/mapObject'
interface IObjectData {
[key: string]: Buffer
}
export default class MapObjectUploadEvent extends BaseEvent {
public listen(): void {
this.socket.on(SocketEvent.GM_MAPOBJECT_UPLOAD, this.handleEvent.bind(this))
this.socket.on('gm:mapObject:upload', this.handleEvent.bind(this))
}
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {

Some files were not shown because too many files have changed in this diff Show More