Compare commits

...

60 Commits

Author SHA1 Message Date
2de92ca51c Moved traefik config fille 2025-02-08 14:59:04 +01:00
2cdcfa7e56 Fix formatting 2025-02-08 14:57:18 +01:00
ee4eca6db3 Added traefik 2025-02-08 14:56:39 +01:00
daca3d306d SSL 2025-02-08 05:01:00 +01:00
47d38e36dd Node > TSX 2025-02-08 04:34:53 +01:00
14b4726546 Rm file 2025-02-08 04:32:03 +01:00
394ad13341 ? 2025-02-08 04:16:15 +01:00
fe18f8b54e docker 2025-02-08 04:09:51 +01:00
11f177c901 Updated Dockerfile 2025-02-08 03:28:55 +01:00
e1f9f8e523 Updated Dockerfile 2025-02-08 03:18:32 +01:00
10c8e493a7 Updated Dockerfile 2025-02-08 03:07:35 +01:00
9a5aa9a53d Run command in tmux 2025-02-08 02:59:49 +01:00
2d5a445af0 Single line 2025-02-08 02:54:07 +01:00
af43faf8f2 Run migrations 2025-02-08 02:47:18 +01:00
cbaadc0c26 Typo 2025-02-08 02:42:45 +01:00
1ed9c2a61f Updated Dockerfile 2025-02-08 02:36:25 +01:00
b46989d3af Updated README.md 2025-02-07 23:26:17 +01:00
30310bf0cf Updated Dockerfile 2025-02-07 23:23:03 +01:00
a28b1b9bee Updated Dockerfile 2025-02-07 23:20:44 +01:00
ccdc39e74b Updated Dockerfile 2025-02-07 23:16:04 +01:00
df860cb7d7 Updated Dockerfile 2025-02-07 23:13:39 +01:00
5e46597e7d Updated Dockerfile 2025-02-07 23:11:19 +01:00
aebe21f140 Updated Dockerfile 2025-02-07 23:03:03 +01:00
d4c1098a5e Updated Dockerfile 2025-02-07 23:00:42 +01:00
8b9afdf956 Updated Dockerfile 2025-02-07 22:57:05 +01:00
5f02aca6e4 Renamed file 2025-02-07 22:53:38 +01:00
e824f0f558 MariaDB fix 2025-02-07 22:38:36 +01:00
13252e056f MySQL > MariaDB 2025-02-07 22:35:54 +01:00
50f595f718 Typo 2025-02-07 22:30:27 +01:00
a7726387af Updated Dockerfile 2025-02-07 22:29:21 +01:00
cff5fed4f7 Removed TSC in favour of node's own Typescript exec. 2025-02-07 22:27:19 +01:00
52b8a9b7ad More typescript improvements 2025-02-07 20:54:55 +01:00
f5e7d10fb4 Updated tsconfig.json and edited all required files to work with it 2025-02-07 20:37:51 +01:00
fae428f239 hm 2025-02-07 20:07:36 +01:00
37f2cd90e6 a 2025-02-07 02:13:10 +01:00
3265bf7823 almost 2025-02-07 02:11:18 +01:00
209f474575 asd
asd
2025-02-07 02:08:20 +01:00
733a1c4956 pff 2025-02-07 02:07:10 +01:00
7e2c5eb529 ¿ 2025-02-07 02:03:12 +01:00
b2f7d45a1f ? 2025-02-07 02:01:16 +01:00
8b0746958e w 2025-02-07 01:55:36 +01:00
0c607fe39d a 2025-02-07 01:53:43 +01:00
aae125c6c6 :) 2025-02-07 01:52:53 +01:00
4ace8c1e84 Build fix attempt 2025-02-07 01:48:42 +01:00
cf3b274cd3 Reverted tsconfig.json, updated package.json 2025-02-07 01:42:54 +01:00
10fd2064ba TS fix 2025-02-07 01:40:01 +01:00
9ba8be51ab Missing package 2025-02-07 01:35:02 +01:00
1686a7a9a0 Higher node version 2025-02-07 01:29:14 +01:00
b82e2fd0fd Build fix 2025-02-07 01:19:44 +01:00
71dd1f240d Added default character type 2025-02-07 01:11:11 +01:00
f2917e67e3 Minor fixes 2025-02-07 01:08:40 +01:00
9ea12ee458 Moved bash code into .sh file 2025-02-07 00:59:59 +01:00
67a4c6763b Added MySQL to dockerfile 2025-02-07 00:54:44 +01:00
f0c0456121 Added titles to error notifications 2025-02-06 21:21:01 +01:00
4992ef69d4 Changed values for smoother movement 2025-02-06 14:07:46 +01:00
765a0468bc Walk improvements 2025-02-06 13:47:02 +01:00
ba96ae7dd4 Minor improvements 2025-02-06 13:12:19 +01:00
9baffd1327 Added title to error notification 2025-02-05 15:13:39 +01:00
a14074afcf Updated packages 2025-02-05 15:10:22 +01:00
6b03937c39 Fixed strings 2025-02-05 02:55:16 +01:00
94 changed files with 2899 additions and 444 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"

View File

@ -1,41 +1,16 @@
# Use the official Node.js 22.4.1 image
FROM node:22.4.1-alpine
FROM node:lts-alpine
# Install Redis and tmux
RUN apk add --no-cache redis tmux
# Install packages
RUN apk update
RUN apk add --no-cache tmux coreutils
# Set the working directory in the container
WORKDIR /usr/src/
WORKDIR /usr/src/app
# Copy package.json and package-lock.json (if available)
COPY package*.json ./
# Install application dependencies
RUN npm install
RUN npm ci
# Copy prisma schema
COPY prisma ./prisma/
# Generate Prisma client
RUN npx prisma generate
# Copy the rest of your application code to the container
COPY . .
# Build the application
RUN npm run build
# Expose the ports your Node.js application and Redis will listen on
EXPOSE 80 6379
# Create a shell script to run Redis, run migrations, and start the application in a tmux session
RUN echo '#!/bin/sh' > /usr/src/start.sh && \
echo 'redis-server --daemonize yes' >> /usr/src/start.sh && \
echo 'npx prisma migrate deploy' >> /usr/src/start.sh && \
echo 'tmux new-session -d -s nodeapp "node dist/server.js"' >> /usr/src/start.sh && \
echo 'echo "App is running in tmux session. Attach with: tmux attach-session -t nodeapp"' >> /usr/src/start.sh && \
echo 'tail -f /dev/null' >> /usr/src/start.sh && \
chmod +x /usr/src/start.sh
# Use the shell script as the entry point
CMD ["/usr/src/start.sh"]
# Modify CMD to use tmux
CMD npx mikro-orm-esm migration:up && npm run start

View File

@ -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 `npx 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 `npx 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 `npx 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 `npx mikro-orm migration:up` to apply all pending migrations.
Run `npx mikro-orm-esm migration:up` to apply all pending migrations.
### Import default data

View File

@ -1,4 +0,0 @@
{
"schemaVersion": 2,
"dockerfilePath" :"./Dockerfile"
}

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:

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"]

View File

@ -1,104 +0,0 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250128165214 extends Migration {
override async up(): Promise<void> {
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`map_effect\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_effect\` add index \`map_effect_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_event_tile\` add index \`map_event_tile_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`map_event_tile_teleport\` (\`id\` varchar(255) not null, \`map_event_tile_id\` varchar(255) not null, \`to_map_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_event_tile_teleport\` add unique \`map_event_tile_teleport_map_event_tile_id_unique\`(\`map_event_tile_id\`);`);
this.addSql(`alter table \`map_event_tile_teleport\` add index \`map_event_tile_teleport_to_map_id_index\`(\`to_map_id\`);`);
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` numeric(10,2) not null default 0, \`origin_y\` numeric(10,2) not null default 0, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`placed_map_object\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_type\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` enum('MALE', 'FEMALE') not null, \`race\` enum('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') not null, \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_hair\` add index \`character_hair_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`sprite_action\` (\`id\` varchar(255) not null, \`sprite_id\` varchar(255) not null, \`action\` varchar(255) not null, \`sprites\` json null, \`origin_x\` numeric(5,2) not null default 0, \`origin_y\` numeric(5,2) not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`frame_rate\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`sprite_action\` add index \`sprite_action_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`tile\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`user\` (\`id\` varchar(255) not null, \`username\` varchar(255) not null, \`email\` varchar(255) not null, \`password\` varchar(255) not null, \`online\` tinyint(1) not null default false, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`user\` add unique \`user_username_unique\`(\`username\`);`);
this.addSql(`alter table \`user\` add unique \`user_email_unique\`(\`email\`);`);
this.addSql(`create table \`password_reset_token\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`token\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`password_reset_token\` add index \`password_reset_token_user_id_index\`(\`user_id\`);`);
this.addSql(`alter table \`password_reset_token\` add unique \`password_reset_token_token_unique\`(\`token\`);`);
this.addSql(`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`map_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character\` add index \`character_user_id_index\`(\`user_id\`);`);
this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`);
this.addSql(`alter table \`character\` add index \`character_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`character\` add index \`character_character_type_id_index\`(\`character_type_id\`);`);
this.addSql(`alter table \`character\` add index \`character_character_hair_id_index\`(\`character_hair_id\`);`);
this.addSql(`create table \`chat\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`message\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`chat\` add index \`chat_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`chat\` add index \`chat_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`character_item\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`item_id\` varchar(255) not null, \`quantity\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_item\` add index \`character_item_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`character_item\` add index \`character_item_item_id_index\`(\`item_id\`);`);
this.addSql(`create table \`character_equipment\` (\`id\` varchar(255) not null, \`slot\` enum('HEAD', 'BODY', 'ARMS', 'LEGS', 'NECK', 'RING') not null, \`character_id\` varchar(255) not null, \`character_item_id\` varchar(255) not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_item_id_index\`(\`character_item_id\`);`);
this.addSql(`create table \`world\` (\`date\` datetime not null, \`rain_percentage\` int not null default 0, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_type\` add constraint \`character_type_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`sprite_action\` add constraint \`sprite_action_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`password_reset_token\` add constraint \`password_reset_token_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_character_type_id_foreign\` foreign key (\`character_type_id\`) references \`character_type\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character\` add constraint \`character_character_hair_id_foreign\` foreign key (\`character_hair_id\`) references \`character_hair\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`chat\` add constraint \`chat_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`chat\` add constraint \`chat_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_item\` add constraint \`character_item_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_item\` add constraint \`character_item_item_id_foreign\` foreign key (\`item_id\`) references \`item\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_item_id_foreign\` foreign key (\`character_item_id\`) references \`character_item\` (\`id\`) on update cascade on delete cascade;`);
}
}

93
package-lock.json generated
View File

@ -5,6 +5,7 @@
"packages": {
"": {
"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",
@ -20,6 +21,7 @@
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
"pino": "^9.3.2",
"reflect-metadata": "^0.2.2",
"sharp": "^0.33.4",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
@ -27,7 +29,6 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@mikro-orm/cli": "^6.4.2",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
@ -542,9 +543,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -603,9 +604,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -636,6 +637,19 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1073,7 +1087,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@jercle/yargonaut/-/yargonaut-1.1.5.tgz",
"integrity": "sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"chalk": "^4.1.2",
@ -1110,7 +1123,6 @@
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.5.tgz",
"integrity": "sha512-Ujmpy6ZFs//2TYzi0Q1tzmrOjq+SwtkT7Iv1RUsniaF21N6R71qhQnSHdkJgVuaGGE5a6Qyp52mDWSwW4qb9EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jercle/yargonaut": "1.1.5",
@ -2040,7 +2052,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -2050,7 +2061,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@ -2368,9 +2378,9 @@
"license": "BSD-3-Clause"
},
"node_modules/bullmq": {
"version": "5.40.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.0.tgz",
"integrity": "sha512-tmrk32EmcbtUOGPSdwlDUcc0w+nAMqCisk8vEFFmG8aOzIehz0BxTNSj6Grh0qoMugRF3VglWk8HGUBnWqU2Fw==",
"version": "5.40.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.2.tgz",
"integrity": "sha512-Cn4NUpwGAF4WnuXR2kTZCTAUEUHajSCn/IqiDG9ry1kVvAwwwg1Ati3J5HN2uZjqD5PBfNDXYnsc2+0PzakDwg==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@ -2453,7 +2463,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@ -2508,7 +2517,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
@ -2927,7 +2935,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@ -3203,18 +3210,18 @@
}
},
"node_modules/eslint": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz",
"integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.10.0",
"@eslint/core": "^0.11.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.19.0",
"@eslint/js": "9.20.0",
"@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@ -3751,7 +3758,6 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz",
"integrity": "sha512-chzvGjd+Sp7KUvPHZv6EXV5Ir3Q7kYNpCr4aHrRW79qFtTefmQZNny+W1pW9kf5zeE6dikku2W50W/wAH2xWgw==",
"dev": true,
"license": "MIT",
"bin": {
"figlet": "bin/index.js"
@ -3972,7 +3978,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@ -4351,9 +4356,9 @@
}
},
"node_modules/ioredis": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz",
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
@ -4457,13 +4462,13 @@
}
},
"node_modules/is-boolean-object": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz",
"integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
"integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"call-bound": "^1.0.3",
"has-tostringtag": "^1.0.2"
},
"engines": {
@ -4565,7 +4570,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -4850,7 +4854,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@ -5308,7 +5311,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -5546,9 +5548,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -5747,7 +5749,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz",
"integrity": "sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
@ -5873,9 +5874,9 @@
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
"integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
@ -6127,7 +6128,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -6758,7 +6758,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@ -6832,7 +6831,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@ -6845,7 +6843,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -6868,7 +6865,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@ -7017,7 +7013,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"json5": "^2.2.2",
@ -7413,7 +7408,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@ -7452,7 +7446,6 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
@ -7468,7 +7461,6 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@ -7487,7 +7479,6 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"

View File

@ -1,18 +1,34 @@
{
"type": "module",
"tsNode": true,
"scripts": {
"start": "node dist/server.js",
"start": "tsx src/server.ts",
"dev": "nodemon --exec tsx src/server.ts",
"build": "tsc",
"format": "prettier --write src/",
"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/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",
"bullmq": "^5.13.2",
@ -23,6 +39,7 @@
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
"pino": "^9.3.2",
"reflect-metadata": "^0.2.2",
"sharp": "^0.33.4",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
@ -30,7 +47,6 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@mikro-orm/cli": "^6.4.2",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",

View File

@ -1,6 +1,6 @@
import fs from 'fs'
import { Response } from 'express'
import type { Response } from 'express'
import Logger, { LoggerType } from '#application/logger'

View File

@ -1,7 +1,8 @@
import { Server } from 'socket.io'
import type { TSocket } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
import { TSocket } from '#application/types'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
@ -25,12 +26,13 @@ export abstract class BaseEvent {
protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)
this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`)
}
protected handleError(context: string, error: unknown): void {
const errorMessage = error instanceof Error ? error.message : String(error)
this.emitError(`${context}: ${errorMessage}`)
this.logger.error('character:connect error', errorMessage)
console.log(error)
const errorMessage = error instanceof Error ? error.message : error && typeof error === 'object' && 'toString' in error ? error.toString() : String(error)
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,6 @@
import { EntityManager } from '@mikro-orm/core'
import Database from '../database'
import Database from '#application/database'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseRepository {

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import pino from 'pino'
const logger = pino.pino
export enum LoggerType {
HTTP = 'http',
@ -13,13 +14,13 @@ export enum LoggerType {
}
class Logger {
private instances: Map<LoggerType, ReturnType<typeof pino>> = new Map()
private instances: Map<LoggerType, pino.Logger> = new Map()
private getLogger(type: LoggerType): ReturnType<typeof pino> {
private getLogger(type: LoggerType): pino.Logger {
if (!this.instances.has(type)) {
this.instances.set(
type,
pino({
logger({
level: process.env.LOG_LEVEL || 'debug',
transport: {
target: 'pino/file',

View File

@ -2,10 +2,11 @@ import fs from 'fs'
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 { UUID } from '#application/types'
import { Character } from '#entities/character'
import { CharacterHair } from '#entities/characterHair'
import { CharacterType } from '#entities/characterType'

View File

@ -1,6 +1,7 @@
import { 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'

View File

@ -1,11 +1,12 @@
import fs from 'fs'
import { 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 { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
@ -26,7 +27,7 @@ export class AvatarController extends BaseController {
* @param res
*/
public async getByName(req: Request, res: Response) {
const character = await this.characterRepository.getByName(req.params.characterName)
const character = await this.characterRepository.getByName(req.params.characterName!)
if (!character?.characterType) {
return this.sendError(res, 'Character or character type not found', 404)
}

View File

@ -1,4 +1,4 @@
import { Request, Response } from 'express'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import CharacterHairRepository from '#repositories/characterHairRepository'

View File

@ -1,4 +1,4 @@
import { Request, Response } from 'express'
import type { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import Storage from '#application/storage'
@ -12,6 +12,10 @@ export class TexturesController extends BaseController {
public async download(req: Request, res: Response) {
const { type, spriteId, file } = req.params
if (!type || !file) {
return this.sendError(res, 'Invalid request', 400)
}
const texture = type === 'sprites' && spriteId ? Storage.getPublicPath(type, spriteId, file) : Storage.getPublicPath(type, file)
this.sendFile(res, texture)

View File

@ -1,16 +1,17 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { CharacterEquipment } from '#entities/characterEquipment'
import { CharacterHair } from '#entities/characterHair'
import { CharacterItem } from '#entities/characterItem'
import { CharacterType } from '#entities/characterType'
import { Chat } from '#entities/chat'
import { Map } from '#entities/map'
import { User } from '#entities/user'
export class BaseCharacter extends BaseEntity {
@PrimaryKey()
@ -28,7 +29,7 @@ export class BaseCharacter extends BaseEntity {
@Property()
role = 'player'
@OneToMany(() => Chat, (chat) => chat.character)
@OneToMany({ mappedBy: 'character' })
chats = new Collection<Chat>(this)
// Position - @TODO: Update to spawn point when current map is not found

View File

@ -2,11 +2,12 @@ import { randomUUID } from 'node:crypto'
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'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { CharacterItem } from '#entities/characterItem'
export class BaseCharacterEquipment extends BaseEntity {
@PrimaryKey()

View File

@ -2,9 +2,10 @@ import { randomUUID } from 'node:crypto'
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 { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'

View File

@ -1,12 +1,12 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { CharacterEquipment } from '#entities/characterEquipment'
import { Item } from '#entities/item'
export class BaseCharacterItem extends BaseEntity {
@PrimaryKey()

View File

@ -2,9 +2,10 @@ import { randomUUID } from 'node:crypto'
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 { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'

View File

@ -1,11 +1,12 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Map } from '#entities/map'
export class BaseChat extends BaseEntity {
@PrimaryKey()

View File

@ -2,9 +2,10 @@ import { randomUUID } from 'node:crypto'
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 { UUID } from '#application/types'
import { CharacterItem } from '#entities/characterItem'
import { Sprite } from '#entities/sprite'

View File

@ -1,12 +1,13 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { MapEffect } from '#entities/mapEffect'
import { MapEventTile } from '#entities/mapEventTile'
import { PlacedMapObject } from '#entities/placedMapObject'
export class BaseMap extends BaseEntity {
@PrimaryKey()
@ -22,7 +23,7 @@ export class BaseMap extends BaseEntity {
height = 10
@Property({ type: 'json', nullable: true })
tiles?: any
tiles: Array<Array<string>> = []
@Property()
pvp = false
@ -33,13 +34,13 @@ export class BaseMap extends BaseEntity {
@Property()
updatedAt = new Date()
@OneToMany(() => MapEffect, (effect) => effect.map, { orphanRemoval: true })
@OneToMany({ mappedBy: 'map', orphanRemoval: true })
mapEffects = new Collection<MapEffect>(this)
@OneToMany(() => MapEventTile, (tile) => tile.map, { orphanRemoval: true })
@OneToMany({ mappedBy: 'map', orphanRemoval: true })
mapEventTiles = new Collection<MapEventTile>(this)
@OneToMany(() => PlacedMapObject, (placedMapObject) => placedMapObject.map, { orphanRemoval: true })
@OneToMany({ mappedBy: 'map', orphanRemoval: true })
placedMapObjects = new Collection<PlacedMapObject>(this)
setId(id: UUID) {

View File

@ -1,10 +1,11 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
export class BaseMapEffect extends BaseEntity {
@PrimaryKey()

View File

@ -1,12 +1,13 @@
import { randomUUID } from 'node:crypto'
import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
export class BaseMapEventTile extends BaseEntity {
@PrimaryKey()
@ -24,7 +25,7 @@ export class BaseMapEventTile extends BaseEntity {
@Property()
positionY!: number
@OneToOne(() => MapEventTileTeleport, (teleport) => teleport.mapEventTile, { eager: true })
@OneToOne({ eager: true })
teleport?: MapEventTileTeleport
setId(id: UUID) {

View File

@ -1,11 +1,12 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTile } from '#entities/mapEventTile'
export class BaseMapEventTileTeleport extends BaseEntity {
@PrimaryKey()

View File

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

View File

@ -1,10 +1,11 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { User } from '#entities/user'
export class BasePasswordResetToken extends BaseEntity {
@PrimaryKey()

View File

@ -1,13 +1,12 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapObject } from '#entities/mapObject'
//@TODO : Rename mapObject
export class BasePlacedMapObject extends BaseEntity {
@PrimaryKey()

View File

@ -2,8 +2,9 @@ import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { SpriteAction } from '#entities/spriteAction'
export class BaseSprite extends BaseEntity {

View File

@ -1,10 +1,11 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
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'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite'
export interface SpriteImage {
url: string

View File

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

View File

@ -3,8 +3,9 @@ import { randomUUID } from 'node:crypto'
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 { UUID } from '#application/types'
import { Character } from '#entities/character'
import { PasswordResetToken } from '#entities/passwordResetToken'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapManager from '#managers/mapManager'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository'

View File

@ -4,6 +4,7 @@ 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'
@ -19,35 +20,39 @@ export default class CharacterCreateEvent extends BaseEvent {
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', { message: 'User not found' })
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', { message: 'Character name already exists' })
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', { message: 'You can only have 4 characters' })
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!).save()
await newCharacter.setName(data.name).setUser(user).setMap(map!).setCharacterType(characterType).save()
if (!newCharacter) {
return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' })
return this.socket.emit('notification', { title: 'Error', message: 'Failed to create character. Please try again (later).' })
}
characters = [...characters, newCharacter]
@ -59,9 +64,9 @@ export default class CharacterCreateEvent extends BaseEvent {
} catch (error: any) {
this.logger.error(`character:create error: ${error.message}`)
if (error instanceof ZodError) {
return this.socket.emit('notification', { message: error.issues[0].message })
return this.socket.emit('notification', { title: 'Error', message: error.issues[0]!.message })
}
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
return this.socket.emit('notification', { title: 'Error', message: 'Could not create character. Please try again (later).' })
}
}
}

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository'
import TeleportService from '#services/characterTeleportService'

View File

@ -1,12 +1,13 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository'
interface IPayload {
id: UUID
}
export default class characterHairDeleteEvent extends BaseEvent {
export default class CharacterHairDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
}
@ -15,13 +16,16 @@ export default class characterHairDeleteEvent extends BaseEvent {
try {
if (!(await this.isCharacterGM())) return
const characterHair = await CharacterHairRepository.getById(data.id)
await (await CharacterHairRepository.getById(data.id))?.delete()
const characterHairRepository = new CharacterHairRepository()
const characterHair = await characterHairRepository.getById(data.id)
if (!characterHair) return callback(false)
await characterHair.delete()
return callback(true)
} catch (error) {
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
this.logger.error(`Error deleting character hair ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -1,6 +1,7 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender } from '#application/enums'
import { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository'
import SpriteRepository from '#repositories/spriteRepository'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface IPayload {

View File

@ -1,6 +1,7 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender, CharacterRace } from '#application/enums'
import { UUID } from '#application/types'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import SpriteRepository from '#repositories/spriteRepository'

View File

@ -1,5 +1,7 @@
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 {
@ -10,8 +12,12 @@ export default class ItemCreateEvent extends BaseEvent {
try {
if (!(await this.isCharacterGM())) return
const spriteRepository = new SpriteRepository()
const sprite = await spriteRepository.getFirst()
if (!sprite) return callback(false)
const newItem = new Item()
await newItem.setName('New Item').setItemType('WEAPON').setStackable(false).setRarity('COMMON').setSprite(null).save()
await newItem.setName('New Item').setItemType(ItemType.WEAPON).setStackable(false).setRarity(ItemRarity.COMMON).setSprite(sprite).save()
return callback(true, newItem)
} catch (error) {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import ItemRepository from '#repositories/itemRepository'
interface IPayload {

View File

@ -1,6 +1,7 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { ItemType, ItemRarity } from '#application/enums'
import { UUID } from '#application/types'
import ItemRepository from '#repositories/itemRepository'
import SpriteRepository from '#repositories/spriteRepository'

View File

@ -1,8 +1,9 @@
import fs from 'fs'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { UUID } from '#application/types'
import MapObjectRepository from '#repositories/mapObjectRepository'
interface IPayload {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapObjectRepository from '#repositories/mapObjectRepository'
type Payload = {
@ -27,19 +28,19 @@ export default class MapObjectUpdateEvent extends BaseEvent {
const mapObject = await mapObjectRepository.getById(data.id)
if (!mapObject) return callback(false)
await mapObject
.setName(data.name)
.setTags(data.tags)
.setOriginX(data.originX)
.setOriginY(data.originY)
.setFrameRate(data.frameRate)
.setFrameWidth(data.frameWidth)
.setFrameHeight(data.frameHeight)
.save()
if (data.name !== undefined) mapObject.name = data.name
if (data.tags !== undefined) mapObject.tags = data.tags
if (data.originX !== undefined) mapObject.originX = data.originX
if (data.originY !== undefined) mapObject.originY = data.originY
if (data.frameRate !== undefined) mapObject.frameRate = data.frameRate
if (data.frameWidth !== undefined) mapObject.frameWidth = data.frameWidth
if (data.frameHeight !== undefined) mapObject.frameHeight = data.frameHeight
await mapObject.save()
return callback(true)
} catch (error) {
console.error(error)
this.socket.emit('notification', { title: 'Error', message: 'Failed to update mapObject.' })
return callback(false)
}
}

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite'
import SpriteRepository from '#repositories/spriteRepository'

View File

@ -1,8 +1,9 @@
import fs from 'fs'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { UUID } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {

View File

@ -2,8 +2,9 @@ import fs from 'fs'
import sharp from 'sharp'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { SpriteAction } from '#entities/spriteAction'
import SpriteRepository from '#repositories/spriteRepository'

View File

@ -1,8 +1,9 @@
import fs from 'fs/promises'
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { UUID } from '#application/types'
import TileRepository from '#repositories/tileRepository'
type Payload = {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import TileRepository from '#repositories/tileRepository'
type Payload = {

View File

@ -1,5 +1,7 @@
import type { MapCacheT } from '#entities/map'
import { BaseEvent } from '#application/base/baseEvent'
import { Map, MapCacheT } from '#entities/map'
import { Map } from '#entities/map'
type Payload = {
name: string
@ -19,12 +21,12 @@ export default class MapCreateEvent extends BaseEvent {
this.logger.info(`GM ${(await this.getCharacter())!.getId()} has created a new map via map editor.`)
if (data.name === '') {
this.socket.emit('notification', { message: 'Map name cannot be empty.' })
this.socket.emit('notification', { title: 'Error', message: 'Map name cannot be empty.' })
return callback(false)
}
if (data.width < 1 || data.height < 1) {
this.socket.emit('notification', { message: 'Map width and height must be greater than 0.' })
this.socket.emit('notification', { title: 'Error', message: 'Map width and height must be greater than 0.' })
return callback(false)
}

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapRepository from '#repositories/mapRepository'
type Payload = {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import MapRepository from '#repositories/mapRepository'

View File

@ -1,6 +1,7 @@
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { MapEventTileType } from '#application/enums'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEffect } from '#entities/mapEffect'
import { MapEventTile } from '#entities/mapEventTile'
@ -62,9 +63,11 @@ export default class MapUpdateEvent extends BaseEvent {
if (data.tiles.length > data.height) {
data.tiles = data.tiles.slice(0, data.height)
}
for (let i = 0; i < data.tiles.length; i++) {
if (data.tiles[i].length > data.width) {
data.tiles[i] = data.tiles[i].slice(0, data.width)
const row = data.tiles[i]
if (row !== undefined && row.length > data.width) {
data.tiles[i] = row.slice(0, data.width)
}
}

View File

@ -1,5 +1,6 @@
import type { MapEventTileWithTeleport } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { MapEventTileWithTeleport } from '#application/types'
import MapManager from '#managers/mapManager'
import MapCharacter from '#models/mapCharacter'
import MapEventTileRepository from '#repositories/mapEventTileRepository'
@ -8,6 +9,8 @@ import TeleportService from '#services/characterTeleportService'
export default class CharacterMove extends BaseEvent {
private readonly characterService = CharacterService
private readonly MOVEMENT_CANCEL_DELAY = 250
private movementTimeouts: Map<string, NodeJS.Timeout> = new Map()
public listen(): void {
this.socket.on('map:character:move', this.handleEvent.bind(this))
@ -20,61 +23,99 @@ export default class CharacterMove extends BaseEvent {
return
}
// If already moving, cancel current movement and wait for it to fully stop
// Clear any existing movement timeout
const existingTimeout = this.movementTimeouts.get(this.socket.characterId!)
if (existingTimeout) {
clearTimeout(existingTimeout)
this.movementTimeouts.delete(this.socket.characterId!)
}
// If already moving, cancel current movement
if (mapCharacter.isMoving) {
mapCharacter.isMoving = false
await new Promise((resolve) => setTimeout(resolve, 100))
mapCharacter.currentPath = null
// Add small delay before starting new movement
await new Promise((resolve) => {
const timeout = setTimeout(resolve, this.MOVEMENT_CANCEL_DELAY)
this.movementTimeouts.set(this.socket.characterId!, timeout)
})
}
// Validate target position is within reasonable range
const currentX = mapCharacter.character.positionX
const currentY = mapCharacter.character.positionY
const distance = Math.sqrt(Math.pow(positionX - currentX, 2) + Math.pow(positionY - currentY, 2))
if (distance > 20) {
// Maximum allowed distance
this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'Target position too far')
return
}
const path = await this.characterService.calculatePath(mapCharacter.character, positionX, positionY)
if (!path) {
if (!path?.length) {
this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'No valid path found')
return
}
// Start new movement
mapCharacter.isMoving = true
mapCharacter.currentPath = path // Add this property to MapCharacter class
mapCharacter.currentPath = path
await this.moveAlongPath(mapCharacter, path)
}
private async moveAlongPath(mapCharacter: MapCharacter, path: Array<{ positionX: number; positionY: number }>): Promise<void> {
const character = mapCharacter.getCharacter()
for (let i = 0; i < path.length - 1; i++) {
if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) {
return
try {
for (let i = 0; i < path.length - 1; i++) {
if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) {
return
}
const [start, end] = [path[i], path[i + 1]]
if (!start || !end) {
this.logger.error('Invalid path step detected')
break
}
// Validate each step
if (Math.abs(end.positionX - start.positionX) > 1 || Math.abs(end.positionY - start.positionY) > 1) {
this.logger.error('Invalid path step detected')
break
}
character.setRotation(CharacterService.calculateRotation(start.positionX, start.positionY, end.positionX, end.positionY))
const mapEventTileRepository = new MapEventTileRepository()
const mapEventTile = await mapEventTileRepository.getEventTileByMapIdAndPosition(character.getMap().getId(), Math.floor(end.positionX), Math.floor(end.positionY))
if (mapEventTile?.type === 'BLOCK') break
if (mapEventTile?.type === 'TELEPORT' && mapEventTile.teleport) {
await this.handleTeleportMapEventTile(mapEventTile as MapEventTileWithTeleport)
return
}
// Update position first
character.setPositionX(end.positionX).setPositionY(end.positionY)
// Then emit with the same properties
this.io.in(character.map.id).emit('map:character:move', {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
rotation: character.getRotation(),
isMoving: true
})
await this.characterService.applyMovementDelay()
}
const [start, end] = [path[i], path[i + 1]]
character.setRotation(CharacterService.calculateRotation(start.positionX, start.positionY, end.positionX, end.positionY))
const mapEventTileRepository = new MapEventTileRepository()
const mapEventTile = await mapEventTileRepository.getEventTileByMapIdAndPosition(character.getMap().getId(), Math.floor(end.positionX), Math.floor(end.positionY))
if (mapEventTile?.type === 'BLOCK') break
if (mapEventTile?.type === 'TELEPORT' && mapEventTile.teleport) {
await this.handleTeleportMapEventTile(mapEventTile as MapEventTileWithTeleport)
return
} finally {
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
this.finalizeMovement(mapCharacter)
}
// Update position first
character.setPositionX(end.positionX).setPositionY(end.positionY)
// Then emit with the same properties
this.io.in(character.map.id).emit('map:character:move', {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
rotation: character.getRotation(),
isMoving: true
})
await this.characterService.applyMovementDelay()
}
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
this.finalizeMovement(mapCharacter)
}
}
@ -96,7 +137,7 @@ export default class CharacterMove extends BaseEvent {
positionX: mapCharacter.character.positionX,
positionY: mapCharacter.character.positionY,
rotation: mapCharacter.character.rotation,
isMoving: false
isMoving: mapCharacter.isMoving
})
}
}

View File

@ -1,6 +1,6 @@
import { Server as SocketServer } from 'socket.io'
import { TSocket } from '#application/types'
import type { TSocket } from '#application/types'
export default class SomeJob {
constructor(private params: any) {}

View File

@ -28,6 +28,11 @@ export class ConsoleManager {
private async processCommand(commandLine: string): Promise<void> {
const [cmd, ...args] = commandLine.trim().split(' ')
if (!cmd) {
console.log('No command provided')
return
}
if (cmd === 'exit') {
this.prompt.close()
return

View File

@ -61,6 +61,7 @@ class DateManager {
if (timeOnlyPattern.test(timeString)) {
const [hours, minutes] = timeString.split(':').map(Number)
if (!hours || !minutes) return null
const newDate = new Date(this.currentDate)
newDate.setHours(hours, minutes)
return newDate

View File

@ -1,5 +1,6 @@
import cors from 'cors'
import { Application } from 'express'
import type { Application } from 'express'
import config from '#application/config'
import { AuthController } from '#controllers/auth'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import LoadedMap from '#models/loadedMap'
import MapCharacter from '#models/mapCharacter'

View File

@ -4,14 +4,15 @@ import { Job, Queue, Worker } from 'bullmq'
import IORedis from 'ioredis'
import { Server as SocketServer } from 'socket.io'
import type { TSocket } from '#application/types'
import config from '#application/config'
import Logger, { LoggerType } from '#application/logger'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import SocketManager from '#managers/socketManager'
class QueueManager {
private connection!: IORedis
private connection!: IORedis.Redis
private queue!: Queue
private worker!: Worker
private io!: SocketServer
@ -20,7 +21,7 @@ class QueueManager {
public async boot() {
this.io = SocketManager.getIO()
this.connection = new IORedis(config.REDIS_URL, {
this.connection = new IORedis.Redis(config.REDIS_URL, {
maxRetriesPerRequest: null
})

View File

@ -2,12 +2,13 @@ import fs from 'fs'
import { Server as HTTPServer } from 'http'
import { pathToFileURL } from 'url'
import { Application } from 'express'
import { Server as SocketServer } from 'socket.io'
import type { TSocket, UUID } from '#application/types'
import type { Application } from 'express'
import Logger, { LoggerType } from '#application/logger'
import Storage from '#application/storage'
import { TSocket, UUID } from '#application/types'
import { Authentication } from '#middleware/authentication'
class SocketManager {

View File

@ -1,6 +1,5 @@
import { User } from '@prisma/client'
import Logger, { LoggerType } from '#application/logger'
import { User } from '#entities/user'
type TLoggedInUsers = {
users: User[]

View File

@ -1,8 +1,9 @@
import { verify } from 'jsonwebtoken'
import jwt from 'jsonwebtoken'
import type { TSocket } from '#application/types'
import config from '#application/config'
import Logger, { LoggerType } from '#application/logger'
import { TSocket } from '#application/types'
class SocketAuthenticator {
private socket: TSocket
@ -39,7 +40,7 @@ class SocketAuthenticator {
}
private verifyToken(token: string): void {
verify(token, config.JWT_SECRET, (err: any, decoded: any) => {
jwt.verify(token, config.JWT_SECRET, (err: any, decoded: any) => {
if (err) {
this.logger.error('Invalid token')
return this.next(new Error('Authentication error'))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
import { Migration } from '@mikro-orm/migrations'
export class Migration20250207212301 extends Migration {
override async up(): Promise<void> {
this.addSql(
`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(
`create table \`map_effect\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`map_effect\` add index \`map_effect_map_id_index\`(\`map_id\`);`)
this.addSql(
`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, \`teleport_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`map_event_tile\` add index \`map_event_tile_map_id_index\`(\`map_id\`);`)
this.addSql(`alter table \`map_event_tile\` add unique \`map_event_tile_teleport_id_unique\`(\`teleport_id\`);`)
this.addSql(
`create table \`map_event_tile_teleport\` (\`id\` varchar(255) not null, \`map_event_tile_id\` varchar(255) not null, \`to_map_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`map_event_tile_teleport\` add unique \`map_event_tile_teleport_map_event_tile_id_unique\`(\`map_event_tile_id\`);`)
this.addSql(`alter table \`map_event_tile_teleport\` add index \`map_event_tile_teleport_to_map_id_index\`(\`to_map_id\`);`)
this.addSql(
`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` numeric(10,2) not null default 0, \`origin_y\` numeric(10,2) not null default 0, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(
`create table \`placed_map_object\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`)
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`)
this.addSql(
`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(
`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`)
this.addSql(
`create table \`character_type\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` enum('MALE', 'FEMALE') not null, \`race\` enum('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') not null, \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`)
this.addSql(
`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`character_hair\` add index \`character_hair_sprite_id_index\`(\`sprite_id\`);`)
this.addSql(
`create table \`sprite_action\` (\`id\` varchar(255) not null, \`sprite_id\` varchar(255) not null, \`action\` varchar(255) not null, \`sprites\` json null, \`origin_x\` numeric(5,2) not null default 0, \`origin_y\` numeric(5,2) not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`frame_rate\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`sprite_action\` add index \`sprite_action_sprite_id_index\`(\`sprite_id\`);`)
this.addSql(
`create table \`tile\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(
`create table \`user\` (\`id\` varchar(255) not null, \`username\` varchar(255) not null, \`email\` varchar(255) not null, \`password\` varchar(255) not null, \`online\` tinyint(1) not null default false, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`user\` add unique \`user_username_unique\`(\`username\`);`)
this.addSql(`alter table \`user\` add unique \`user_email_unique\`(\`email\`);`)
this.addSql(
`create table \`password_reset_token\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`token\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`password_reset_token\` add index \`password_reset_token_user_id_index\`(\`user_id\`);`)
this.addSql(`alter table \`password_reset_token\` add unique \`password_reset_token_token_unique\`(\`token\`);`)
this.addSql(
`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`map_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`character\` add index \`character_user_id_index\`(\`user_id\`);`)
this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`)
this.addSql(`alter table \`character\` add index \`character_map_id_index\`(\`map_id\`);`)
this.addSql(`alter table \`character\` add index \`character_character_type_id_index\`(\`character_type_id\`);`)
this.addSql(`alter table \`character\` add index \`character_character_hair_id_index\`(\`character_hair_id\`);`)
this.addSql(
`create table \`chat\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`message\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`chat\` add index \`chat_character_id_index\`(\`character_id\`);`)
this.addSql(`alter table \`chat\` add index \`chat_map_id_index\`(\`map_id\`);`)
this.addSql(
`create table \`character_item\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`item_id\` varchar(255) not null, \`quantity\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`character_item\` add index \`character_item_character_id_index\`(\`character_id\`);`)
this.addSql(`alter table \`character_item\` add index \`character_item_item_id_index\`(\`item_id\`);`)
this.addSql(
`create table \`character_equipment\` (\`id\` varchar(255) not null, \`slot\` enum('HEAD', 'BODY', 'ARMS', 'LEGS', 'NECK', 'RING') not null, \`character_id\` varchar(255) not null, \`character_item_id\` varchar(255) not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_id_index\`(\`character_id\`);`)
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_item_id_index\`(\`character_item_id\`);`)
this.addSql(
`create table \`world\` (\`date\` datetime not null, \`rain_percentage\` int not null default 0, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`
)
this.addSql(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(
`alter table \`map_event_tile\` add constraint \`map_event_tile_teleport_id_foreign\` foreign key (\`teleport_id\`) references \`map_event_tile_teleport\` (\`id\`) on update cascade on delete set null;`
)
this.addSql(
`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(
`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(
`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`)
this.addSql(`alter table \`character_type\` add constraint \`character_type_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`)
this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`)
this.addSql(`alter table \`sprite_action\` add constraint \`sprite_action_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(
`alter table \`password_reset_token\` add constraint \`password_reset_token_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(`alter table \`character\` add constraint \`character_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(`alter table \`character\` add constraint \`character_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade;`)
this.addSql(
`alter table \`character\` add constraint \`character_character_type_id_foreign\` foreign key (\`character_type_id\`) references \`character_type\` (\`id\`) on update cascade on delete set null;`
)
this.addSql(
`alter table \`character\` add constraint \`character_character_hair_id_foreign\` foreign key (\`character_hair_id\`) references \`character_hair\` (\`id\`) on update cascade on delete set null;`
)
this.addSql(`alter table \`chat\` add constraint \`chat_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(`alter table \`chat\` add constraint \`chat_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(
`alter table \`character_item\` add constraint \`character_item_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(`alter table \`character_item\` add constraint \`character_item_item_id_foreign\` foreign key (\`item_id\`) references \`item\` (\`id\`) on update cascade on delete cascade;`)
this.addSql(
`alter table \`character_equipment\` add constraint \`character_equipment_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`
)
this.addSql(
`alter table \`character_equipment\` add constraint \`character_equipment_character_item_id_foreign\` foreign key (\`character_item_id\`) references \`character_item\` (\`id\`) on update cascade on delete cascade;`
)
}
}

View File

@ -3,12 +3,12 @@ import { Migrator } from '@mikro-orm/migrations'
import { defineConfig, MySqlDriver } from '@mikro-orm/mysql'
import { TsMorphMetadataProvider } from '@mikro-orm/reflection'
import serverConfig from './src/application/config'
import serverConfig from '#application/config'
export default defineConfig({
extensions: [Migrator],
metadataProvider: TsMorphMetadataProvider,
entities: ['./src/entities/*.js'],
entities: ['./dist/entities/*.js'],
entitiesTs: ['./src/entities/*.ts'],
driver: MySqlDriver,
host: serverConfig.DB_HOST,
@ -21,7 +21,7 @@ export default defineConfig({
allowPublicKeyRetrieval: true
},
migrations: {
path: './migrations',
pathTs: './migrations',
path: './dist/migrations',
pathTs: './src/migrations'
}
})
})

View File

@ -1,8 +1,8 @@
import MapCharacter from './mapCharacter'
import type { UUID } from '#application/types'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Map } from '#entities/map'
import MapCharacter from '#models/mapCharacter'
import MapEventTileRepository from '#repositories/mapEventTileRepository'
class LoadedMap {
@ -47,7 +47,7 @@ class LoadedMap {
// Set the grid values based on the event tiles, these are strings
eventTiles.forEach((eventTile) => {
if (eventTile.type === 'BLOCK') {
grid[eventTile.positionY][eventTile.positionX] = 1
grid[eventTile.positionY]![eventTile.positionX] = 1
}
})

View File

@ -1,9 +1,9 @@
import { Server } from 'socket.io'
import { TSocket, UUID } from '#application/types'
import type { TSocket, UUID } from '#application/types'
import { Character } from '#entities/character'
import MapManager from '#managers/mapManager'
import SocketManager from '#managers/socketManager'
import TeleportService from '#services/characterTeleportService'
class MapCharacter {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { CharacterHair } from '#entities/characterHair'
class CharacterHairRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
class CharacterRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { CharacterType } from '#entities/characterType'
class CharacterTypeRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Chat } from '#entities/chat'
class ChatRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Item } from '#entities/item'
class ItemRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { MapEventTile } from '#entities/mapEventTile'
class MapEventTileRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { MapObject } from '#entities/mapObject'
class MapObjectRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTile } from '#entities/mapEventTile'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { PasswordResetToken } from '#entities/passwordResetToken'
class PasswordResetTokenRepository extends BaseRepository {

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite'
class SpriteRepository extends BaseRepository {
@ -23,6 +24,18 @@ class SpriteRepository extends BaseRepository {
return []
}
}
async getFirst(populate?: any): Promise<Sprite | null> {
try {
const repository = this.getEntityManager().getRepository(Sprite)
const result = await repository.findOne({ id: { $exists: true } }, { populate })
if (result) result.setEntityManager(this.getEntityManager())
return result
} catch (error: any) {
return null
}
}
}
export default SpriteRepository

View File

@ -1,7 +1,6 @@
import { FilterValue } from '@mikro-orm/core'
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { unduplicateArray } from '#application/utilities'
import { Map } from '#entities/map'
import { Tile } from '#entities/tile'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { User } from '#entities/user'
class UserRepository extends BaseRepository {

View File

@ -1,7 +1,10 @@
import 'reflect-metadata'
import { createServer as httpServer, Server as HTTPServer } from 'http'
import cors from 'cors'
import express, { Application } from 'express'
import express from 'express'
import type { Application } from 'express'
import config from '#application/config'
import Database from '#application/database'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseService } from '#application/base/baseService'
import { UUID } from '#application/types'
import MapManager from '#managers/mapManager'
import SocketManager from '#managers/socketManager'

View File

@ -7,7 +7,8 @@ type Position = { positionX: number; positionY: number }
export type Node = Position & { parent?: Node; g: number; h: number; f: number }
class CharacterMoveService extends BaseService {
private readonly MOVEMENT_DELAY_MS = 260
private readonly MOVEMENT_DELAY_MS = 200
private readonly MAX_PATH_LENGTH = 20 // Limit maximum path length
private readonly DIRECTIONS = [
{ x: 0, y: -1 }, // Up
@ -29,6 +30,7 @@ class CharacterMoveService extends BaseService {
return null
}
// Ensure we're working with valid coordinates
const start: Position = {
positionX: Math.floor(character.positionX),
positionY: Math.floor(character.positionY)
@ -39,7 +41,26 @@ class CharacterMoveService extends BaseService {
positionY: Math.floor(targetY)
}
return this.findPath(start, end, grid)
// Don't calculate path if start and end are the same
if (start.positionX === end.positionX && start.positionY === end.positionY) {
return null
}
// Add maximum distance check
const directDistance = Math.sqrt(Math.pow(targetX - character.positionX, 2) + Math.pow(targetY - character.positionY, 2))
if (directDistance > this.MAX_PATH_LENGTH) {
return null
}
const path = this.findPath(start, end, grid)
// Validate path length
if (path.length > this.MAX_PATH_LENGTH) {
return path.slice(0, this.MAX_PATH_LENGTH)
}
return path
}
public calculateRotation(X1: number, Y1: number, X2: number, Y2: number): number {
@ -112,15 +133,15 @@ class CharacterMoveService extends BaseService {
return (
pos.positionX >= 0 &&
pos.positionY >= 0 &&
pos.positionX < grid[0].length &&
pos.positionX < grid[0]!.length &&
pos.positionY < grid.length &&
(grid[pos.positionY][pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY))
(grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY))
)
}
private getDistance(a: Position, b: Position): number {
const dx = Math.abs(a.positionX - b.positionX),
dy = Math.abs(a.positionY - b.positionY)
const dx = Math.abs(a.positionX - b.positionX)
const dy = Math.abs(a.positionY - b.positionY)
// Manhattan distance for straight paths, then Euclidean for diagonals
return dx + dy + (Math.sqrt(2) - 2) * Math.min(dx, dy)
}

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import MapManager from '#managers/mapManager'
import SocketManager from '#managers/socketManager'

View File

@ -1,5 +1,6 @@
import type { UUID } from '#application/types'
import { BaseService } from '#application/base/baseService'
import { UUID } from '#application/types'
import { Chat } from '#entities/chat'
import SocketManager from '#managers/socketManager'
import CharacterRepository from '#repositories/characterRepository'

View File

@ -1,43 +1,51 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"allowJs": true,
"declaration": true,
// Best practices
"strict": true,
// Base options
"esModuleInterop": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"lib": ["es2022"],
"target": "es2022",
// Strictness
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Transpiling with TypeScript
"module": "NodeNext",
"baseUrl": "./src",
"rootDir": "./src",
"outDir": "./dist",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"#application/*": ["./src/application/*"],
"#commands/*": ["./src/commands/*"],
"#entities/*": ["./src/entities/*"],
"#controllers/*": ["./src/controllers/*"],
"#jobs/*": ["./src/jobs/*"],
"#managers/*": ["./src/managers/*"],
"#middleware/*": ["./src/middleware/*"],
"#models/*": ["./src/models/*"],
"#repositories/*": ["./src/repositories/*"],
"#services/*": ["./src/services/*"],
"#events/*": ["./src/events/*"]
"#root/*": ["./*"],
"#application/*": ["application/*"],
"#commands/*": ["commands/*"],
"#entities/*": ["entities/*"],
"#controllers/*": ["controllers/*"],
"#jobs/*": ["jobs/*"],
"#managers/*": ["managers/*"],
"#middleware/*": ["middleware/*"],
"#models/*": ["models/*"],
"#repositories/*": ["repositories/*"],
"#services/*": ["services/*"],
"#events/*": ["events/*"]
},
// Specify multiple folders that act like './node_modules/@types'
"typeRoots": ["./node_modules/@types"],
// Other options
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}